Commit Graph
233 Commits
Author SHA1 Message Date
enricobuehlerandClaude Opus 5 19deac75fe feat(packaging): a desktop-login host restarts with its desktop
The shipped unit's PartOf=punktfunk-kde-session.service covers the
APPLIANCE route, where we start the compositor ourselves. A host on a
machine somebody logs into has no such unit: when Plasma or GNOME
restarts, the daemon keeps running while holding a Wayland socket and a
portal D-Bus connection that both died with the old compositor, and it
cannot recover either in-process. It fails quietly — the host still
listens, still answers, and every session it then serves dies at capture.
A host that mirrors a monitor idles for days between sessions, which is
the shape that finds this at the worst moment.

The drop-in binds the host to graphical-session.target: PartOf takes it
down with the session, WantedBy brings it back with the new one. Shipped
under /usr/share rather than as an active drop-in, because it is wrong
for the appliance route (which may never reach that target at all, and
would then leave the host permanently stopped) — the operator opts in.

Closes the restart half of design/per-monitor-portal-capture.md §6.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 23:51:41 +02:00
enricobuehlerandClaude Fable 5 ab58fd2f0e feat(vdisplay/driver): DDC/CI against the virtual monitor fails fast
In exclusive topology the virtual display is the ONLY monitor on the desktop,
so monitor-control software (the Twinkle Tray / PowerToys PowerDisplay /
Monitorian class) aims its entire DDC traffic — brightness polls,
capabilities-string requests — at OUR monitor. There is no bus and no sink;
the only wrong answer is a slow one (a timeout-shaped failure occupies
win32k's physical-monitor path, serialized per monitor, for its full
duration). Register EvtIddCxMonitorI2CTransmit/Receive and answer every probe
with an immediate STATUS_NOT_SUPPORTED, making the virtual monitor the
cheapest thing on the "bus" to poll. (The receive DDI has no out-arg at the
callback — refusing synchronously refuses the whole transaction.) EDID needs
no equivalent: the OS serves descriptor queries from the blob supplied at
monitor creation without calling the driver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:37:06 +02:00
enricobuehlerandClaude Fable 5 19d6f79d2d feat(vdisplay/driver): the frame pump survives MMCSS refusal and outranks GPU contention
Two stall-immunity hardenings for the swap-chain drain thread (branch-2 of the
disturbance-immunity program — failures in OUR delivery leg, as opposed to
adapter-wide display servicing, which no priority survives):

- MMCSS registration fails under the restricted WUDFHost token on some boxes;
  the drain thread then ran UNPRIORITIZED — the whole display's frame pump at
  normal priority, starvable by DDC/HPD-servicing DPC pressure into
  multi-hundred-ms delivery holes. Fall back to TIME_CRITICAL.

- IddCxSetRealtimeGPUPriority (IddCx 1.9) raises the processing device above
  every regular application's GPU priority. Slot availability comes from
  raising the exported IddMinimumVersionRequired 4 → 10, which was overdue
  independently: the drain loop already calls ReleaseAndAcquireBuffer2 (1.10)
  unconditionally, so binding a pre-1.10 framework would dispatch past the
  populated IddFunctions table — with 10 an old framework fails the bind
  cleanly instead. The product floor is already Win11 22H2 (installer
  MinVersion=10.0.22621, whose framework is 1.10), so no supported system
  changes behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:24:10 +02:00
enricobuehler 94b7a834cb fix(installer/windows): GameStream is opt-in, not on by default
apple / swift (push) Successful in 3m19s
decky / build-publish (push) Successful in 35s
windows-host / package (push) Successful in 18m44s
windows-host / winget-source (push) Skipped
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 22m27s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m15s
apple / screenshots (push) Successful in 23m24s
ci / web (push) Successful in 58s
ci / docs-site (push) Successful in 1m27s
ci / bench (push) Successful in 7m15s
ci / rust-arm64 (push) Successful in 11m47s
deb / build-publish-client-arm64 (push) Successful in 10m21s
android / android (push) Failing after 11m52s
deb / build-publish-host (push) Successful in 11m34s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 1m42s
deb / build-publish (push) Successful in 17m34s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3m6s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8m30s
arch / build-publish (push) Successful in 18m29s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 13m59s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 14m36s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 13m1s
docker / deploy-docs (push) Successful in 35s
ci / rust (push) Successful in 30m15s
docker / build-push-arm64cross (push) Successful in 9m8s
A user found this in their log and had never been offered a choice:

  WARN GameStream/Moonlight compat ENABLED (--gamestream): its pairing runs over
  plain HTTP and its legacy control encryption can reuse GCM nonces
  (security-review #5/#9) — an on-path LAN attacker could MITM pairing or
  recover input. Enable only on a TRUSTED network.

They were right. The `gamestream` task carried no `Flags: unchecked`, so it was
pre-ticked; and since a silent install takes the wizard's own task defaults
(1839d756), a winget install turned it on with no checkbox ever shown. The host
warns that this plane is a security downgrade on every single start, so having
the installer enable it by default cannot be squared with our own advice — least
of all on the path where nobody sees a wizard.

Now unchecked, matching `allowpublicfw`, the other task with a security
consequence. A default install therefore lands `PUNKTFUNK_HOST_CMD=serve`, the
native-only host; Punktfunk's own clients are unaffected, since the native
punktfunk/1 plane is always on. Unattended opt-in is `/MERGETASKS=gamestream`.

Scope, deliberately: this changes FRESH installs only. `GamestreamParam` already
omits the flag entirely unless `FreshHostInstall`, so no existing host has its
choice touched by an upgrade, and anyone relying on Moonlight today keeps it.
`DEFAULT_HOST_CMD` (the service's fallback when host.env carries no
PUNKTFUNK_HOST_CMD line at all) is deliberately NOT flipped here: the installer
always writes the line explicitly, so it does not affect a normal install, and
inverting it would silently disable Moonlight for anyone whose host.env lost that
line — a runtime regression that belongs in its own change, if at all.

Docs corrected everywhere they stated the old default, since three of them now
told users the opposite of what ships: the winget Agreement shown BEFORE install
(it said "enabled by default" and gave the opt-OUT override), the Windows
packaging README, and host.env.example. The `--override` example in the installer
manifest is inverted with it (`/MERGETASKS=gamestream` to add, `!` to remove).

Verified: [Tasks] + [Code] compile with ISCC 6.7.1 on the windows-amd64 runner
(all optional features defined), and the winget catalogue rebuilds and passes
29/29 with the edited manifests.
2026-07-27 12:26:47 +02:00
enricobuehler 4114dfeff7 fix(winget): the Log switch used |LOGPATH|, which winget never substitutes
Second field report on 0.20.0's winget path, and independent of the conflict
abort in 37781a61: an INTERACTIVE install through UniGetUI dies before the
wizard with Inno's "Error creating log file: The filename, directory name, or
volume label syntax is incorrect."

`<LOGPATH>` is winget's own token and the only spelling it replaces — the
schema this manifest declares says so verbatim ("<LOGPATH> token can be included
in the switch value so that winget will replace the token with user provided
path"). We shipped `|LOGPATH|`, which is not a token, so winget passed the
literal string to Inno; `|` is not legal in a Windows filename, and Inno refuses
before doing anything else.

Worse than the conflict abort in two ways: it is not confined to silent installs
— it fires in EVERY mode for any caller that requests a log — and UniGetUI
requests one by default, so a GUI user cannot install at all. Confirmed against
the live source, which is serving the broken switch to everyone right now:
  curl -s https://winget.punktfunk.unom.io/packageManifests/unom.PunktfunkHost
  → "Log": "/LOG=\"|LOGPATH|\""

Guarded by the suite that exists for exactly this class of defect (a wrong
response shape does not fail loudly — winget just says "no package found"). The
new check accepts an absent Log switch, requires <LOGPATH> when present, and
rejects any |TOKEN| spelling. Mutation-verified: restoring |LOGPATH| fails the
check naming the offending value, and the corrected manifest passes 29/29.

⚠ Fixing the template does NOT fix the live catalogue: it is derived from the
manifest trio attached to each release, so the corrected switch only reaches
users when a release carries it.
2026-07-27 10:28:59 +02:00
enricobuehler 37781a610f fix(installer/windows): only a host that will actually RUN is a conflict
docker / deploy-docs (push) Successful in 41s
ci / web (push) Successful in 56s
ci / docs-site (push) Successful in 1m50s
apple / swift (push) Successful in 5m41s
ci / bench (push) Successful in 7m1s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 2m3s
ci / rust-arm64 (push) Successful in 9m51s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 27s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 27s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 30s
deb / build-publish-host (push) Successful in 11m32s
deb / build-publish-client-arm64 (push) Successful in 10m59s
decky / build-publish (push) Successful in 41s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 30s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6m19s
android / android (push) Successful in 17m31s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 5m35s
windows-host / package (push) Successful in 11m21s
windows-host / winget-source (push) Skipped
arch / build-publish (push) Successful in 19m9s
docker / build-push-arm64cross (push) Successful in 5m7s
ci / rust (push) Successful in 22m25s
deb / build-publish (push) Successful in 10m49s
apple / screenshots (push) Successful in 23m8s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 21m34s
Field report within hours of 0.20.0 (HitFrostbite): `winget install
unom.PunktfunkHost` fails with "Installer failed with exit code: 1",
0x8A150006, after the hash verifies and the install starts.

That is Setup aborting on purpose. `StreamHostPresent` counted a conflicting
host as present if its SCM service key existed at ANY start type, OR if a bare
Program Files\<Name> directory existed — so a disabled service, or a folder left
behind by an uninstall, read as a live conflict. 3d893013 then made the warning
a SuppressibleMsgBox defaulting to IDNO so unattended installs could not hang on
it; under winget's /VERYSILENT /SUPPRESSMSGBOXES that default aborts
InitializeSetup, and Inno exits "failed to initialize" = code 1. Net effect:
anyone with Sunshine on disk — the exact audience most likely to migrate — could
not install via winget at all, with no visible reason.

The other candidate for exit 1 is excluded rather than assumed: the manifest
declares MinimumOSVersion 10.0.22621.0 matching the script's MinVersion, so
winget refuses an unsupported OS BEFORE downloading, and the report shows the
hash verified and the install starting. The conflict check is also the only
`Result := False` in InitializeSetup.

Detection now keys on the service's `Start` value (0 boot / 1 system /
2 automatic / 3 manual / 4 disabled), counting only 0-2 — a host that comes up
on its own is the only kind that can take the GameStream ports or load a second
virtual-display driver. Directories no longer count at all: they were the
noisiest signal and the one that survives an uninstall. This makes the installer
agree with the tray, which dropped its always-on warning over a merely-INSTALLED
Sunshine in this same release (3e782852) for exactly the same reason. The
message now says "installed and set to start automatically" and offers disabling
the service as a remedy, both of which are what is actually detected.

A conflicting host running as a plain user process is deliberately not detected:
pure Pascal cannot enumerate processes, it is a runtime rather than install-time
condition, and the host already reports it via the `punktfunk::detect` startup
warning, `detect-conflicts`, and /api/v1/local/summary.

Verified by compiling the [Code] section with ISCC 6.7.1 on the windows-amd64
runner (WithWeb defined, as CI does). That caught two defects in my own first
draft that review would not have: a continuation line STARTING with #13#10, which
ISPP reads as a preprocessor directive ("Unknown preprocessor directive", compile
aborted), and `{commonpf}` written inside a `{ }` comment, which closes it early
since Inno comments do not nest. Both are noted at the sites so they are not
reintroduced.
2026-07-27 09:00:34 +02:00
enricobuehlerandClaude Opus 5 1839d7566b feat(packaging/winget): install the Windows host with winget, from our own source
docker / build-push-arm64cross (push) Successful in 10s
docker / deploy-docs (push) Successful in 31s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 26m54s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 26m56s
apple / screenshots (push) Canceled after 11m30s
windows-host / package (push) Failing after 12s
windows-host / winget-source (push) Skipped
apple / swift (push) Successful in 6m2s
ci / web (push) Successful in 1m53s
ci / docs-site (push) Successful in 1m29s
ci / rust-arm64 (push) Successful in 9m56s
android / android (push) Successful in 12m33s
ci / bench (push) Successful in 7m17s
decky / build-publish (push) Successful in 29s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 19s
arch / build-publish (push) Successful in 18m18s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 16s
deb / build-publish (push) Successful in 9m2s
ci / rust (push) Failing after 19m38s
deb / build-publish-client-arm64 (push) Successful in 7m21s
deb / build-publish-host (push) Successful in 9m43s
`winget install unom.PunktfunkHost` after adding the source, and `winget upgrade`
from then on. Windows had no update path at all before this — no self-update, no
package manager — so keeping a host current meant noticing a release and
re-running an installer by hand.

Ships the manifest trio in winget-pkgs' own format (so submitting upstream later
is a copy, not a rewrite) plus a release-time generator that substitutes only
version, URL, hash and release-notes link. Everything reviewable — the silent
switches, the agreements, the installation notes — stays in the checked-in
templates rather than buried in a generator.

Silent installs deliberately take the SAME task defaults the wizard shows: a
per-channel default is a support trap. The disclosures the wizard puts on screen
travel as manifest Agreements instead, shown before install and requiring
acceptance — VB-Audio's bundling grant wants the user to see VB-CABLE's origin
and donationware status, and a silent install shows them nothing otherwise. The
console password can only be pointed at, never printed: it is generated per
install and the notes are fixed at publish time.

Self-hosted rather than the community repo, which gates on Defender/SmartScreen
validation the self-signed installer cert would not clear today. The source is
three endpoints — /information, /manifestSearch, /packageManifests/{id}. The
reference implementation's other twenty are its admin API for mutating a
CosmosDB; a catalogue generated at release time has nothing to mutate.

It runs on unom-1 as a stock bun image with the two .mjs files bind-mounted, the
same shape as the flatpak server, so there is no image to build or pull. The
catalogue is derived from the RELEASES rather than local files: winget resolves
--version and upgrade against the version list, so a source that only knew the
newest release could neither pin an older one nor show an upgrade path from it.
That also leaves no state to drift — re-running the build reproduces it exactly.

NormalizedPackageNameAndPublisher is declared unsupported on purpose. winget
derives it client-side with its own normalization, and a near-miss silently
mis-correlates an installed host; ProductCode is exact and Inno gives us one.

28 checks drive the handler directly, and CI gates on them before anything
reaches the box — a wrong response shape does not fail loudly, it just makes
winget report "no package found".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:09:13 +02:00
enricobuehlerandClaude Opus 5 3d89301336 fix(installer/windows): a silent install can't hang on a prompt, and an upgrade keeps your choices
Three faults that only bite when nobody is watching the wizard — which is how a
package manager always runs us, and how a re-run by hand half-behaves today.

The conflicting-host warning used a plain MsgBox. That ignores /SUPPRESSMSGBOXES
and displays even under /VERYSILENT, so an unattended install onto a box already
running Sunshine/Apollo would sit on a modal dialog nobody can see or click.
SuppressibleMsgBox with an IDNO default aborts instead, which is the honest
answer for a combination the message itself calls unsupported.

The GameStream and Public-firewall choices were re-applied on every run. A silent
run has no wizard, so every task falls back to its script default and the
installer confidently re-asserted it: a user who enabled GameStream had it turned
off again, and one who opened the firewall on Public networks had it quietly
revoked (that task is default-unchecked, so the reset ran the other way). Both
params are now fresh-install-only, keyed off whether host.env already exists.
`service install` already read an absent --gamestream as "keep host.env as-is";
--allow-public-network becomes tri-state the same way, resolving an absent flag
from the marker the previous install recorded. It is strict on the value: a
typo'd =of must not fall through to a marker that may say true, because that
turns a mistyped opt-OUT into leaving Public open.

Also brands the installer "Punktfunk Host" in the strings a user actually sees —
Add/Remove Programs, the Start Menu group, the wizard task text. Paths are
untouched; renaming those would relocate installs and orphan existing config.

Verified on the windows-amd64 runner: ISCC compiles the script (with WithWeb
defined, so the password page parses too), and cargo check + clippy -D warnings
pass for punktfunk-host with nvenc,amf-qsv,qsv.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:09:13 +02:00
enricobuehlerandClaude Opus 5 0d9d78398c fix(drivers/windows): name each virtual pad for what it is — one shared description read as "the setting did nothing"
deb / build-publish (push) Successful in 9m31s
windows-drivers / probe-and-proto (push) Successful in 42s
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 1m9s
windows-drivers / driver-build (push) Successful in 2m1s
ci / bench (push) Successful in 7m35s
decky / build-publish (push) Successful in 20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
deb / build-publish-host (push) Successful in 10m4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
android / android (push) Successful in 12m30s
ci / rust-arm64 (push) Successful in 13m55s
apple / swift (push) Successful in 5m28s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m41s
deb / build-publish-client-arm64 (push) Successful in 11m7s
windows-host / package (push) Successful in 16m55s
arch / build-publish (push) Successful in 20m3s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8m46s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9m7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10m54s
docker / deploy-docs (push) Successful in 34s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m4s
flatpak / build-publish (push) Failing after 8m23s
docker / build-push-arm64cross (push) Successful in 3m59s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m28s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m48s
ci / rust (push) Successful in 32m19s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m55s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m9s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m53s
release / apple (push) Successful in 29m51s
apple / screenshots (push) Successful in 24m26s
`pf_dualsense.inx` gave all four hardware ids a single %DeviceDesc%, so Device
Manager labelled an emulated DualShock 4, DualSense Edge and Steam Deck pad
"punktfunk Virtual DualSense". The HID layer was always per-type — device_type
picks the PID (09CC for DS4), the report descriptor and the product string — but
the one place a user goes to check said DualSense for every choice, which reads
exactly like the controller-type setting being ignored.

Split into four model lines over the same install section, one description each.
No binding, service or descriptor change; stampinf's 9.9.MMdd.HHmm DriverVer
increments on every build, so pnputil takes the update. InfVerif on the WDK
runner: INF is VALID.

Also correct the Slot.pref comment from the previous commit: emulating a
DualShock 4 gives up adaptive triggers by construction. HidOutput::Trigger is
emitted only by dualsense_proto, and a DS4 has no trigger-effect reports — the
host never generates any to send. Rumble and the lightbar remain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 16:56:44 +02:00
enricobuehlerandClaude Opus 5 aff169e131 build(flatpak): make the manifest architecture-generic
Two things were x86-specific, both now expressed properly instead of
hardcoded:

  * PKG_CONFIG_PATH carries the runtime's multiarch directory.
    flatpak-builder does NOT shell-expand `env` values, so ${FLATPAK_ARCH}
    would be taken literally — a build-options.arch override supplies the
    aarch64 string and inherits the rest.

  * The prebuilt Skia archive is per-target and pinned by sha256. Two
    `type: file` sources discriminated by only-arches now share one
    dest-filename, so SKIA_BINARIES_URL stays a single literal path.
    Upstream publishes the aarch64 archive under the same skia commit hash
    and the same resolved-feature key, so the two stay in lockstep on a
    skia-safe bump.

build-flatpak.sh gains ARCH (default: this machine's), passes --arch to
both flatpak-builder and build-bundle, and arch-suffixes the bundle name so
an x86_64 and an aarch64 build can coexist in dist/ instead of the second
silently overwriting the first. CI composes its own published filename, so
nothing downstream changes.

The aarch64 Skia sha256 was verified by downloading the published archive,
not taken from the API. No aarch64 flatpak has been built — flatpak-builder
builds in a sandbox for the target arch, which needs an arm64 machine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:32:45 +02:00
enricobuehlerandClaude Opus 5 4dc078692d build(arch): build the client for aarch64 (Arch Linux ARM)
arch=('x86_64' 'aarch64'), and on aarch64 `pkgname` drops punktfunk-host
so makepkg never enters the host's build or package path; build() takes a
client-only cargo invocation without the NVENC/Vulkan-encode features.
Dropping the host from pkgname rather than giving package_punktfunk-host a
per-package arch is what makes the skip unambiguous, and it mirrors how
PF_WITH_WEB already extends that array.

The host stays x86-only because its encode stack (NVENC/QSV/AMF) is.

Verified by sourcing the PKGBUILD under both CARCH values: x86_64 yields
`punktfunk-host punktfunk-client`, aarch64 yields `punktfunk-client`. NOT
verified by a real build — there is no official arm64 Arch container, and
the README says so rather than implying it is tested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:32:45 +02:00
enricobuehlerandClaude Opus 5 1c76b8c7b4 build(rpm): let the spec build a client-only aarch64 RPM
The blocker was never ExclusiveArch — it was that %build builds host +
tray + client together and %install lays down the host unconditionally,
with no way to express "client only". `%bcond_without host` (default ON,
so an x86_64 build is unchanged) now gates the host binary, the tray, the
headless-session data, the firewalld services, the main %post and the bare
%files section. Omitting %files for the MAIN package is the load-bearing
part: it is what stops rpm emitting an empty `punktfunk` alongside
punktfunk-client.

build-rpm.sh exposes it as PF_WITHOUT_HOST=1 and skips the libcuda stub
regeneration and the libcuda leak check, neither of which means anything
without the host.

Verified two ways. `rpmspec -P` shows the default expansion still carrying
3 host install lines, the tray build and one bare %files, while
--without host carries none of those and one %files client. And a real
rpmbuild in a native aarch64 Fedora 43 container produced
punktfunk-client-*.aarch64.rpm with correct aarch64 sonames
(libc.so.6(GLIBC_2.17), libSDL3.so.0, libavcodec.so.61) and no main
package.

Not a cross-compile: %build runs cargo for the build machine's arch, so
this wants an arm64 builder. No CI leg yet — deliberate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:32:45 +02:00
enricobuehlerandClaude Opus 5 cac82d13df ci: check the client for aarch64 on every push
Adds a `rust-arm64` job: clippy -D warnings for the client crates at
--target aarch64-unknown-linux-gnu, plus a --no-default-features session
build so the minimal embedded configuration keeps compiling in its own
right rather than only as a subset of the default one. Runs in the cross
image on the ordinary amd64 runner; client crates are listed explicitly
because --workspace would drag in the x86-only host encode stack.

This is a regression guard, not an artifact leg (deb.yml ships those).
c_char signedness cuts both ways and neither direction is visible from an
x86-only CI: hardcoding i8 fails to COMPILE on ARM, while casting to u8
fails the LINT on ARM. Two such defects were already found this way.

Also documents the arm64 client .deb recipe in packaging/debian/README.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:32:45 +02:00
enricobuehlerandClaude Opus 5 000f8b85b0 build(ci): cross-compile and publish an aarch64 Linux client
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>
2026-07-25 14:32:45 +02:00
enricobuehlerandClaude Fable 5 8d5a9f66c9 fix(gamescope): ship a packaged privilege path for the DM-stop takeover
ci / web (push) Successful in 52s
ci / docs-site (push) Successful in 55s
android / android (push) Successful in 12m5s
decky / build-publish (push) Successful in 25s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 46s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
ci / bench (push) Successful in 6m29s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m5s
docker / deploy-docs (push) Successful in 27s
deb / build-publish (push) Successful in 8m40s
arch / build-publish (push) Successful in 14m51s
deb / build-publish-host (push) Successful in 9m15s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 15m32s
apple / swift (push) Successful in 23m28s
ci / rust (push) Successful in 26m17s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m37s
apple / screenshots (push) Successful in 2h32m51s
Field report (Nobara, 0.19.2): entering Game Mode mid-stream left the monitor
on and the stream mirroring at the desktop's resolution. Root cause: the
DM-flavor takeover needs privilege to stop plasmalogin, the polkit rule is a
docs-only manual step nobody installs, so every managed entry silently
degraded to ATTACH — and with a physical display connected the attach path
mirrors the box's own session at its own mode.

Ship the privilege with the packages instead: a root helper
(libexec/punktfunk/pf-dm-helper, verbs stop|restore) behind its own polkit
action (io.unom.punktfunk.dm-helper, allow_any — the same mechanism Nobara's
os-session-select uses, and the helper derives the DM unit from the
display-manager.service symlink itself so callers never name a unit across
the privilege boundary). The host tries the plain system-bus verbs first
(root / operator rule still take precedence), then pkexec's the helper; the
restore paths (idle restore + in-stream desktop-switch honor) use the same
ladder so a takeover that needed the helper can always be undone by it.

Packaged in rpm/deb/arch (Arch under /usr/lib/punktfunk with the policy's
exec.path annotation rewritten; the host probes both layouts). Nix is left
out deliberately: store paths can't match the probe, NixOS keeps the manual
rule. Docs updated — the rule snippet stays as the scoped alternative.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 23:41:29 +02:00
enricobuehlerandClaude Fable 5 6617275387 docs(env): stop teaching the compositor pin + uid-1000 anchors in starters
ci / docs-site (push) Successful in 54s
ci / web (push) Successful in 59s
apple / swift (push) Successful in 1m21s
decky / build-publish (push) Successful in 28s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 25s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 14s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
ci / bench (push) Successful in 6m23s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 4m43s
apple / screenshots (push) Successful in 6m18s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m9s
arch / build-publish (push) Successful in 13m44s
deb / build-publish (push) Successful in 13m48s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7m39s
deb / build-publish-host (push) Successful in 13m11s
docker / deploy-docs (push) Successful in 32s
android / android (push) Successful in 16m23s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m15s
ci / rust (push) Successful in 24m12s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m55s
Field triage (Nobara, Discord): the kde.md starter host.env told desktop
users to set PUNKTFUNK_COMPOSITOR=kwin, which PINS the backend — detect()
short-circuits and the capture-loss rebuild never re-detects — so a
mid-stream switch to Game Mode killed the stream instead of following it.
A follow-up hardcoded XDG_RUNTIME_DIR=/run/user/1000 anchor broke PipeWire
for any non-1000 uid (pw audio connect: Creation failed).

Revamp across every starter/example/reference:
- Desktop starters (kde/gnome/hyprland/sway) shrink to
  PUNKTFUNK_VIDEO_SOURCE=virtual + an explicit warning that pinning
  disables session-following; forcing a backend is CI/appliance-only.
- host.env.example: rewritten around auto-detection; anchors demoted to a
  commented ssh/cron-only block with the uid trap spelled out; the
  gamescope ATTACH/MANAGED knobs documented (previously missing);
  case-sensitivity called out.
- packaging/bazzite/host.env + README: drop the uid-1000 anchors (a
  systemctl --user service inherits/derives them); README's stale
  PUNKTFUNK_COMPOSITOR=gamescope-era template synced to the real one.
- packaging/kde/host.env: loud APPLIANCE-ONLY header (it pins on purpose).
- configuration.md: session-anchors section inverted to "leave unset",
  compositor row states the pin consequence, case-sensitivity note.
- troubleshooting.md: new "session fails right after editing host.env"
  section (case, wrong-uid anchors, stale pin, restart-to-apply).
- gamescope.md/bazzite.md: attach/managed descriptions match current
  behavior (managed is the infra-detected default; attach re-modes a
  box-owned session to the client's resolution).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:42:59 +02:00
enricobuehlerandClaude Fable 5 248e1cbf08 fix(pf-vdisplay/driver): report cursor_excluded adapter-wide — the declare's exclusion is not per-target
ci / web (push) Successful in 51s
ci / docs-site (push) Successful in 1m5s
apple / swift (push) Successful in 1m20s
windows-drivers / probe-and-proto (push) Successful in 1m2s
decky / build-publish (push) Successful in 28s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 21s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
windows-drivers / driver-build (push) Successful in 2m23s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m5s
ci / bench (push) Successful in 6m3s
apple / screenshots (push) Successful in 6m41s
deb / build-publish (push) Successful in 12m20s
arch / build-publish (push) Successful in 12m47s
deb / build-publish-host (push) Successful in 13m7s
android / android (push) Successful in 16m14s
windows-host / package (push) Successful in 15m53s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m23s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m8s
ci / rust (push) Successful in 26m6s
docker / deploy-docs (push) Canceled after 25s
Field report (iPad via GameStream, 2026-07-23): cursor missing from the
stream. The session's fresh target 257 reported cursor_excluded=false
(DECLARED_TARGETS is keyed per target and only 259 had declared), so the
host skipped forced-composite — but DWM's pointer exclusion after an
IddCx hardware-cursor declare reaches EVERY later monitor of the adapter,
not just the declaring target (proven on-glass: declare on 259 under
WUDFHost 2192, then 257's frames streamed pointer-free with CURSOR_SHOWING
set and SM_MOUSEPRESENT true). GameStream/Moonlight clients cannot draw a
forwarded cursor, so those sessions were silently cursor-less.

ADD replies now report any-declare-this-WUDFHost-life (the state's real
scope — it dies with the adapter reset, same as before); per-target ids
stay only as the dbglog audit trail. Channel-less sessions on a declared
adapter then hit the existing forced-composite gate (GDI poller + blend)
and carry the pointer in-video. Host code unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:58:54 +02:00
enricobuehlerandClaude Fable 5 7058647264 fix(vdisplay/capture): channel-less sessions composite the pointer on a sticky-declared target
A declared IddCx hardware cursor is IRREVOCABLE for its OS target's life
(§8.6), and the sticky exclusion survives monitor REMOVE→ADD because each
client gets a STABLE target id — so once any desktop-mode session declared,
every later pure-capture session on that target streamed a cursor-less
desktop: DWM excluded the pointer, no channel forwarded it, no blend drew it
(the exact no-regression gap §8.6's per-session cap gate cannot see).

Driver: track every successful SetupHardwareCursor per target
(DECLARED_TARGETS — scoped to the WUDFHost's life, exactly the sticky
state's scope) and report it in a new AddReply::cursor_excluded tail field
(dual-size discipline, both skews degrade cleanly; no proto bump).

Host: the flag rides AddedMonitor → Monitor → WinCaptureTarget; a session
WITHOUT the cursor channel on a flagged target forces composite mode in the
IDD-push capturer — GDI poller + blend for the session's life, pinned on
(set_cursor_forward cannot clear it: with no client drawing, un-compositing
would erase the pointer entirely).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 23:47:53 +02:00
enricobuehlerandClaude Opus 4.8 89cfef429e fix(windows/vdisplay): cursor-render state sticky per TARGET across monitor generations
On-glass: 're-setup on swap-chain assign -> 0x0' fired AFTER 'enable=0
stored' — duplicate/successor monitor entries (re-arrival churn) kept
their default-true flag and re-declared the hardware cursor right after
the composite flip's re-commit, undoing it.

The desired state now lives in a per-target static
(CURSOR_FORWARD_DESIRED): every arrival inherits it (no generation can
resurrect a declare the session turned off), and the flip stamps EVERY
live entry matching the target, declaring against whichever has the
live worker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Opus 4.8 b50e4767de fix(windows/cursor-flip): flag + forced same-mode re-commit — the working un-declare
The empty-caps un-declare is REJECTED by IddCx (STATUS_INVALID_PARAMETER,
on-glass driver 9.9.0722.1407) — there is no un-declare DDI. The
composite flip now works with the only lever the OS gives us: every
mode COMMIT reverts the path to the software cursor, and the driver
skips its per-commit re-declare while cursor_forward_on is off. So:

- driver: disable stores the flag only (no DDI call); enable still
  declares immediately against the live worker's event.
- host capturer: after a successful disable flip, force a same-mode
  re-commit (win_display::force_mode_reenumeration) — DWM composites
  the pointer from the very next commit; the swap-chain flap is the
  class the driver's preserved-publisher machinery already rides out.
- state now survives re-arrivals end-to-end: the capturer caches the
  APPLIED state (cleared on every channel (re)delivery, which
  re-created driver entries need — their flag defaults to declared),
  and the stream loop re-applies every tick instead of edge-gating
  (steady-state cost: one Option compare).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Opus 4.8 6a8df2ba97 fix(windows/cursor-flip): survive driver-side monitor re-arrival — flag-based flip + channel re-delivery
On-glass (.173, match-window session): every window-size convergence
re-creates the driver-side monitor (re-arrival resize / sibling-session
slot churn), destroying the cursor worker the channel was delivered to —
the flip then died NOT_FOUND (no entry with target_id ∧ worker) and the
declared state was lost entirely.

Driver: set_cursor_forward becomes STATE, not an edge on one monitor
generation — find by target_id ∧ hw_cursor, store cursor_forward_on
even without a live worker (it steers the next delivery/re-setup);
declare/un-declare only when a worker exists. Channel delivery honors
the flag: setup_and_spawn(declare=false) adopts + spawns WITHOUT
declaring (composite mode), so a later enable-flip declares against the
worker's event.

Host: retain the SET_CURSOR_CHANNEL sender; extract
deliver_cursor_channel and RE-deliver the surviving section on every
ring recreate and — with one retry — when a flip IOCTL fails
(the re-arrived entry gets a fresh worker, declared per its flag).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Opus 4.8 030a779391 feat(windows/vdisplay): the driver leg of the mid-stream cursor-render flip
Belongs to 493f4fae (which shipped only the crates/ side — this
packaging/ half was left unstaged, so the deployed driver silently
lacked the IOCTL and every flip died NOT_FOUND at dispatch):
IOCTL_SET_CURSOR_FORWARD dispatch + monitor::set_cursor_forward
(cursor_forward_on flag, resetup gate) + cursor_worker::
unsetup_hardware_cursor (empty-caps un-declare experiment).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Opus 4.8 9162e7b451 fix(windows/vdisplay): drop now-unused INFINITE import (poll-loop wait)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Opus 4.8 d2cc770938 fix(windows/vdisplay): QueryHardwareCursor3 + per-mode-commit cursor re-setup — M2c findings
Durable results of the M2c on-glass bring-up (.173, 2026-07-22):

- The base IddCxMonitorQueryHardwareCursor DDI slot is stubbed to
  STATUS_NOT_SUPPORTED on WDK 26100 — add the ...QueryHardwareCursor3
  wrapper (wdk-iddcx) and drain it instead; v3 X/Y are only meaningful
  when PositionValid, so a position-invalid tick keeps the prior position.
- Hardware-cursor setup is per-mode-commit: the OS silently reverts to a
  software cursor on every mode commit, so re-issue the setup on each
  swap-chain assignment (monitor::resetup_cursor, called from
  assign_swap_chain) or the query fails NOT_SUPPORTED forever.
- One caps definition for initial setup and re-setup, resting at XOR
  FULL: the query delivers ONLY alpha shapes in every configuration
  (all three ColorXorCursorSupport levels, event-driven and 30 Hz
  polled) — masked/monochrome cursors never arrive. FULL keeps the
  frame cursor-free for ALL cursor types; the full-fidelity shape comes
  from the session-side cursor source in the host
  (design/remote-desktop-sweep.md §8).
- Poll the query at ~30 Hz instead of pure event-wait: masked cursors
  never fire the data event, and the seqlock's position/visibility
  should stay fresh regardless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Fable 5 3a34440a6b feat(windows): IddCx hardware-cursor channel — remote-desktop sweep M2c
Brings the cursor channel to Windows hosts. The pf-vdisplay driver
declares an IddCx hardware cursor for sessions that negotiated
cursor-forward — DWM then EXCLUDES the pointer from the IDD frame and
delivers shape/position out-of-band, into the same CursorOverlay →
forwarder → wire → client pipeline the Linux portal path uses.

- pf-driver-proto v5 (additive, host floor stays 3): AddRequest's spare
  tail becomes hw_cursor (same size/offsets); IOCTL_SET_CURSOR_CHANNEL
  delivers a host-created CursorShm section (64-byte seqlock header +
  256² shape buffer, layout pinned + tested). No event crosses the
  boundary — the host polls at encode-tick pace.
- driver: wdk-iddcx grows the two cursor DDI wrappers; a per-monitor
  cursor worker (event wait → QueryHardwareCursor → seqlock publish)
  starts only when BOTH the ADD asked and the channel arrived, so a
  failed delivery leaves DWM compositing as today. Shape bytes ship raw
  (BGRA/masked + pitch); the host converts.
- host: the section rides the existing sealed-channel broker (least-
  privilege dup, remote reap on failure); IddPushCapturer::cursor()
  seqlock-reads → CursorOverlay (BGRA→RGBA, masked-color approximation,
  desktop→frame origin shift, per-shape conversion cache). New
  Capturer::cursor() trait hook — the encode loop prefers it over the
  frame-attached overlay because hardware-cursor moves produce NO new
  frame on a static desktop. hw_cursor survives the re-arrival resize
  (carried on the manager's Monitor).
- negotiation: cursor_forward grows the Windows arm (client cap ∧
  driver proto ≥ 5, probed once via the control device); SessionPlan
  carries cursor_forward → OutputFormat.hw_cursor.
- drive-by: wdk-probe's two pre-existing same-type casts (clippy) and
  pf-vdisplay's stale spike-test refs (crate::win_display moved to
  pf-win-display; tracing-subscriber was never a dep) repaired.

Verified: proto tests on Mac AND MSVC (15/15 incl. the CursorShm
layout pin); clippy -D warnings for proto/frame/capture/vdisplay/host
on Linux (.21) and native Windows (.173); the DRIVER workspace clippy
-D warnings green against the real WDK 10.0.26100 bindgen (DDI names,
enum variants and IDARG layouts all bind). On-box driver deploy +
on-glass validation follow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Opus 4.8 4094f6208d fix(web): load the inlang plugin from node_modules, not a CDN
audit / bun-audit (push) Successful in 13s
ci / docs-site (push) Successful in 54s
ci / web (push) Successful in 56s
apple / swift (push) Successful in 1m19s
audit / cargo-audit (push) Successful in 3m6s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 55s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
apple / screenshots (push) Successful in 6m41s
deb / build-publish (push) Successful in 10m14s
ci / bench (push) Successful in 10m59s
windows-host / package (push) Successful in 11m11s
deb / build-publish-host (push) Successful in 13m54s
android / android (push) Successful in 17m22s
arch / build-publish (push) Successful in 17m37s
ci / rust (push) Successful in 22m53s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m22s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m48s
docker / deploy-docs (push) Successful in 29s
`web/project.inlang/settings.json` pointed `modules` at
https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js —
inlang's stock `paraglide-js init` scaffolding, which nobody had a reason to
keep. It made `bun run codegen` need the network at build time, so in a
sandbox that has none (the Nix build; any air-gapped CI) the plugin import
failed and paraglide compiled ZERO messages.

That failure is silent by design: the inlang SDK logs a PluginImportError as a
WARNING, paraglide still prints "Successfully compiled" and exits 0, vite
bundles the empty message set, and the console only dies at request time inside
renderToReadableStream because every `m.foo()` is undefined. Reported from a
NixOS host running punktfunk-web-server.

The plugin is a normal npm package, so depend on it like one: add
@inlang/plugin-message-format as a devDependency and reference it by relative
path. `loadProjectFromDirectory` (which both `paraglide-js compile` and the
vite plugin use) imports non-`http` modules straight off disk, and the shipped
dist/index.js is a self-contained bundle with no imports of its own, so it
loads from the data: URL exactly as the CDN copy did. Compiles 268 messages
offline, with no project.inlang/cache round-trip.

Add tools/check-i18n.mjs, run at the end of `codegen`, so this can never ship
silently again: it fails the build on a remote `modules` entry, a module that
does not resolve, or a compiled message count below what messages/en.json
defines. Verified it catches all three (remote URL, missing plugin, corrupt
plugin that compiles to 0 messages) and passes a healthy build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:43:39 +02:00
enricobuehlerandClaude Fable 5 84c47cd0a7 feat(security): run the Windows plugin runner as LocalService, not SYSTEM
The PunktfunkScripting scheduled task ran operator-installed plugin code
as SYSTEM with -RunLevel Highest — any plugin defect was a full
compromise of the box. The principal is now NT AUTHORITY\LocalService
(minimal privileges, no password to manage, exists at boot, loopback
networking works), registered without RunLevel:

- installer: New-ScheduledTaskPrincipal -UserId 'LocalService'
- plugins enable: converges the principal idempotently (migrating tasks
  an older installer or a dev box registered as SYSTEM) BEFORE starting,
  then grants LocalService read — via icacls, full-System32-path — on
  exactly the two SYSTEM/Admins-DACL'd files the runner's connect()
  needs: the scoped plugin-token and the TLS-pin cert.pem. Never
  mgmt-token. plugins disable revokes the grants; plugins status now
  prints the task principal so the migration is verifiable.
- build-scripting.ps1 mirrors the convergence + grants on dev deploys.

The usage text also mentions the new --allow-public-registry gate that
lands with the supply-chain commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 00:45:27 +02:00
enricobuehlerandClaude Fable 5 f0c511c8fa feat(plugins): punktfunk-host plugins CLI — add/remove/list/enable/disable/status
One-liner plugin management replacing the manual scripting-dir + bunfig +
bun-add ritual: package ops forward to the bun runner (new sdk plugins
module + runner-cli subcommands, 11 tests green), enable/disable/status
drive the systemd unit on Linux and the PunktfunkScripting scheduled task
on Windows (installer support in the ISS). Docs page rewritten as .mdx
with per-platform Tabs (registered in mdx.tsx).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:34:43 +02:00
enricobuehlerandClaude Opus 4.8 efbcc4cdb6 fix(packaging): ship packages@unom.io as the maintainer, not noreply@anthropic.com
android-screenshots / screenshots (push) Successful in 4m1s
ci / rust (push) Successful in 26m27s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 6m54s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m15s
decky / build-publish (push) Successful in 20s
apple / swift (push) Successful in 1m21s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m37s
android / android (push) Successful in 14m8s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 6m58s
deb / build-publish (push) Successful in 10m58s
ci / web (push) Successful in 54s
apple / screenshots (push) Successful in 6m19s
ci / docs-site (push) Successful in 1m6s
ci / bench (push) Successful in 5m16s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
docker / deploy-docs (push) Successful in 9s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
release / apple (push) Successful in 10m54s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
deb / build-publish-host (push) Successful in 10m40s
linux-client-screenshots / screenshots (push) Successful in 7m14s
web-screenshots / screenshots (push) Successful in 2m49s
flatpak / build-publish (push) Failing after 8m19s
windows-host / package (push) Successful in 16m34s
arch / build-publish (push) Successful in 12m31s
Every Linux package declared its maintainer as `noreply@anthropic.com` — the
git co-author trailer address, copied out of commit metadata into PACKAGE
metadata, where it is user-visible:

  * deb:   `Maintainer:` in the host/client/web/scripting control files, plus
           the generated debian/changelog trailer  (`dpkg -s punktfunk`)
  * rpm:   all three %changelog entries                (`rpm -q --changelog`)
  * arch:  the PKGBUILD Maintainer comment

So installed packages named Anthropic as the maintainer of this project, and
pointed anyone with a packaging problem at an address that discards mail.

packages@unom.io is the project's real packaging identity — it's already the
EdDSA key (AF245C506F4E4763, "punktfunk packages") that sign-rpms.sh signs
every RPM with, and it's what packaging/rpm/README and the bootc Containerfile
document. The RPMs were therefore SIGNED by packages@unom.io while DECLARING
noreply@anthropic.com; those now agree.

Checked the other publisher surfaces — the Inno installer (AppPublisher=unom),
the flatpak metainfo (developer id io.unom) and the decky package.json were
already correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 11:21:05 +02:00
enricobuehlerandClaude Opus 4.8 168040005a fix(packaging/rpm): correct the bogus %changelog weekday (Jul 17 2026 was a Friday)
rpmbuild reported "bogus date in %changelog: Thu Jul 17 2026 ... - 0.0.1-3"
among its errors on the Fedora builds. 2026-07-17 was a Friday. The other two
entries (Mon Jun 15, Wed Jun 10) check out.

Surfaced underneath the pf-capture compile failure in the same job, so it
would have become the next rpm blocker once that was fixed rather than
showing up on its own.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 11:18:57 +02:00
enricobuehlerandClaude Opus 4.8 1e7c18b2c8 fix(packaging): install punktfunk-host on Ubuntu 24.04 LTS via a noble builder that bundles FFmpeg 8
ci / web (push) Successful in 58s
ci / docs-site (push) Successful in 52s
apple / swift (push) Successful in 1m20s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 5m31s
arch / build-publish (push) Successful in 11m54s
apple / screenshots (push) Successful in 6m29s
android / android (push) Successful in 14m53s
deb / build-publish (push) Successful in 9m6s
deb / build-publish-host (push) Successful in 12m9s
ci / rust (push) Successful in 19m5s
docker / deploy-docs (push) Successful in 26s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 23m55s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 23m46s
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>
2026-07-18 11:30:19 +02:00
enricobuehlerandClaude Opus 4.8 e7a8cf2bba feat(packaging): nix web console + plugin/script runner everywhere; fix tray zbus/tokio crash
ci / web (push) Successful in 58s
apple / swift (push) Successful in 1m24s
ci / docs-site (push) Successful in 59s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 17s
decky / build-publish (push) Successful in 25s
windows-host / package (push) Successful in 19m46s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 17s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 17s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 18s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 17s
android / android (push) Successful in 16m12s
arch / build-publish (push) Successful in 12m14s
ci / bench (push) Successful in 5m27s
apple / screenshots (push) Successful in 6m32s
ci / rust (push) Successful in 19m26s
deb / build-publish (push) Successful in 12m58s
docker / deploy-docs (push) Successful in 24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 24m51s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 24m51s
Ship the management web console and the plugin/script runner across the packaging
matrix, and fix a Linux tray startup crash.

- nix: add punktfunk-web (Nitro SSR on bun) and punktfunk-scripting (Effect runner),
  with services.punktfunk.{web,scripting} modules. Both bun node_modules come from
  fixed-output derivations pinned to real deps hashes; patchShebangs the web
  node_modules so orval/paraglide/vite run in the sandbox. Validated on x86_64-linux
  (nix build .#punktfunk-{web,scripting,tray,host} all green).

- punktfunk-scripting now ships for deb/rpm/arch/nix/windows, mirroring the web-console
  mechanics (vendored bun on deb/rpm/arch, pkgs.bun on nix; one self-contained
  `bun build --target=bun` bundle). Opt-in: host Recommends it, but the systemd --user
  unit ships un-started (Windows registers its scheduled task disabled).

- fix(tray): build punktfunk-tray in its OWN cargo invocation on nix/rpm/arch. Cargo
  unifies features across one `cargo build`, so co-building the tray with the host pulls
  the host's ashpd->zbus/tokio onto the tray's shared zbus and it panics at startup
  ("there is no reactor running, must be called from the context of a Tokio 1.x
  runtime"). Isolated, the tray's zbus stays on async-io. The .deb already did this;
  runtime-verified on nix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 21:16:34 +02:00
enricobuehlerandClaude Fable 5 9da38b8d9c ci(windows-host): build + clippy with the native QSV feature; encode-crate path triggers; CMake 4 policy floor
--features nvenc,amf-qsv,qsv on build and clippy (static VPL dispatcher, no new
DLLs — ffmpeg *_qsv stays as the fallback until Phase 4). Adds pf-encode/** +
libvpl-sys/** to the paths trigger (drift from the W6 split: encoder changes
only reached this workflow via Cargo.lock luck). Exports
CMAKE_POLICY_VERSION_MINIMUM=3.5: audiopus_sys' vendored opus fails a FRESH
CMake-4 configure (observed on this runner on a clean target dir), so green
runs were one cache purge from breaking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:01:05 +02:00
enricobuehlerandClaude Fable 5 600693914f fix(security): land the 2026-07 audit fixes — SSRF guards, roster lane, SYSTEM path hygiene
The low/medium findings from the July host+Windows security review, as
implemented in the audit session's working tree:

- Webhooks and library-art fetches refuse loopback/link-local/metadata
  targets and no longer follow redirects (SSRF pivots from the privileged
  host process).
- The paired-client rosters (/clients, /native/clients) move off the
  streaming-client auth lane — one paired device could enumerate every other
  device's name + fingerprint; only the bearer/loopback console keeps them.
- Device-name sanitizing extends to bidi/format control characters
  (spoofable rendering) via the shared native_pairing::is_spoofy_char;
  stream-marker quoting uses the same set.
- The SYSTEM service resolves powershell by its full System32 path —
  CreateProcess checks the launching EXE's own directory first, so a planted
  powershell.exe beside the host binary would have run as SYSTEM.
- The pf-vdisplay driver's opt-in file log moves from world-writable
  C:\Users\Public to WUDFHost's own temp dir.
- GameStream pairing sessions are single-use (removed whatever the outcome).
- Uninstall also removes the pf_mouse driver-store entry (rider from the
  virtual-HID-mouse work).
- openapi.json regenerated (hardened-config-dir doc wording).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:20:15 +02:00
enricobuehlerandClaude Opus 4.8 09849906e9 Merge perf/first-frame-latency: driver proto v4 + first-frame/resize latency (P0-P2)
Brings the first-frame-latency branch (P0.1 transition tracing, P1.1/P1.2
Welcome-time display prep, P2 in-place resize; pf-driver-proto v3 -> v4 with
IOCTL_UPDATE_MODES) onto current main. The branch predates the W6.2/W7 splits,
so git's rename detection carried most of it into the moved crates
(pf-capture idd_push, pf-vdisplay manager/pf_vdisplay, pf-win-display,
pf-driver-proto, the driver workspace) and the punktfunk1.rs remainder was
re-homed by hand:

- native/handshake.rs: welcome/start trace marks + the Welcome-time display
  prep spawn (the prep thread BECOMES the stream thread; hand-off via a
  SyncSender<SessionContext>). negotiate() gains bringup/quit/stop and returns
  the PrepHandle.
- native.rs: bringup/resize_ms creation + the stop/quit flags hoisted BEFORE
  the handshake (the close watcher splits: flags pre-handshake, lifecycle
  events post-handshake where `hello` exists); punch_done stamp; the data
  plane adopts the prep thread's result or builds inline.
- native/stream.rs: SessionContext/SendStats carry the trace; send_loop
  finishes it on the first video packet; the resize path gains the in-place
  fast path (try_inplace_resize) with the full rebuild as fallback, restructured
  so both share the post-rebuild bookkeeping; prepare_display/PreparedDisplay/
  PrepHandle; build_pipeline(+retry) thread the stage marks.
- session_status/mgmt: ttff_ms + last_resize_ms per session (union with the
  lifecycle-events fields main added to the same spots).
- pf-capture: Capturer gains capture_target_id() + resize_output() defaults.
- pf-vdisplay manager: perf's faster activation poll (60x50ms) + the settle
  floor before the PnP sweep, on main's knobs/no-trait shape.

Also: packaging/windows/build-gamepad-drivers.ps1 is ASCII again (an em-dash
from the pf-mouse work tripped windows-host.yml's locale-safety gate on main).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:08:16 +02:00
enricobuehlerandClaude Fable 5 9e6fc6e071 fix(host/inject,drivers): rumble root fixes A-C — lossless report ring + rumble-keyed idle watchdogs
B: PadFeedback.game_drove -> rumble_drove, keyed on vibration-asserting reports — an
LED/adaptive-trigger stream can no longer feed the abandoned-rumble force-off while a
coalesced stop never re-asserts (the confirmed unbounded stuck-ON path). C: Linux parity —
every UHID backend now arms the shared watchdog (Steam Input drives these pads over hidraw
with Windows abandonment semantics) and the uinput mixer force-stops abandoned
infinite-replay FF effects (FfState, unit-tested). Shared PUNKTFUNK_RUMBLE_IDLE_MS hatch
(0 = off; non-zero floored above SDL's ~2 s rumble resend).

A: PadShm v2.1 — a 1024 B tail extension carrying an 8-slot lossless output-report ring,
feature-negotiated via zeroed reserved fields (out_ring_ver; deliberately NO
GAMEPAD_PROTO_VERSION bump — mixed generations degrade to the legacy latest-report slot
instead of failing closed). The pf-dualsense driver dual-writes both planes
(publish_output); the host's shared OutputDrain drains oldest->newest with a torn-read
recheck and an overflow->resync path (PadFeedback.resync force-stops + re-arms dedups).
pf-umdf-util grows a min_data_size map fallback. Ds*Feedback.fresh removed (dead).

design/rumble-root-fix.md par. A-C. Verified: pf-inject tests+clippy Linux+Windows (53/53
on winbox incl. the stop-coalesce repro); drivers ws check+clippy on the CI runner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 14:07:32 +02:00
enricobuehlerandClaude Fable 5 85dd2bb077 feat(host/windows): resident virtual HID mouse (pf-mouse UMDF minidriver)
Headless Windows hosts (no dongle) stream an INVISIBLE cursor: with no
pointing device present win32k reports SM_MOUSEPRESENT=0 and DWM never
composites a pointer into the pf-vdisplay frame, even though SendInput
moves it. Keep ONE virtual HID mouse devnode alive for the host's
lifetime — the Sunshine/Parsec-class fix, zero client changes.

- pf-mouse: UMDF2 HID minidriver, one fixed identity (PF:MO 5046:4D4F,
  obviously virtual, nothing fingerprints it), one 8-byte input report
  (5 buttons + absolute 15-bit X/Y + wheel + AC-pan). Transport is the
  sealed pad channel verbatim (Global\pfmouse-boot-0 mailbox + unnamed
  MouseShm DATA section) so pf-umdf-util's audited layer serves it
  unchanged; report delivery is event-driven (idle = no HID traffic).
- host: inject::mouse_windows — VirtualMouse (SwDeviceCreate'd devnode +
  channel), ensure_resident() keeper thread started by every
  InjectorService (process-wide, PUNKTFUNK_NO_VIRTUAL_MOUSE opts out),
  vmouse-spike on-glass validation (cursor sweep via HID reports).
- proto: mouse module (magic, boot-name, identity, report layout,
  unit-tested input_report packing).
- SwDeviceProfile grows container_tag so the mouse's ContainerId family
  (PFMO) never groups with a pad's (PFDS) in the Devices UI.
- packaging: pf-mouse rides the gamepad-driver build + install pipeline
  (build-gamepad-drivers.ps1, windows-drivers.yml, driver install
  --gamepad picks up every staged .inf).

On-glass validated on winbox: devnode + HID child bind, SM_MOUSEPRESENT=1
with no physical mouse, cursor sweeps via HID reports (vmouse-spike).

This work was implemented in a parallel session; committed here as the
build prerequisite for the HID compose kick that follows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 11:02:10 +02:00
enricobuehlerandClaude Fable 5 3d9b329084 fix(host): name the lid-closed/no-frames failure — display-write decode, console-session guard, driver-truth attach diagnostics
Field report (Windows laptop, lid closed, Tailscale): v0.12.0's activation
fix works — the pf-vdisplay target activates in ~200ms — but the session
still dies at the first-frame gate: 'driver_status=1 but no frame published
within 4s'. Triage showed three independent blind spots; this names all of
them at their source instead of guessing downstream:

- pf-win-display: decode ChangeDisplaySettingsExW failures (-1 FAILED — a
  display write rejected, the wrong/remote-session signature — vs -2 BADMODE,
  which the old 'mode not advertised?' text conflated), and WARN on every
  non-zero SetDisplayConfig rc in the CCD isolate even when verification
  passes vacuously (the lid-closed case: nothing else active, so the INFO
  swallowed rc=0x5 ERROR_ACCESS_DENIED while the load-bearing COMMIT_MODES →
  ASSIGN_SWAPCHAIN re-commit silently never applied). Access-denied rcs get
  the remedy appended (console session / installed service).

- host: console-session guard (interactive::console_session_mismatch) — a
  host outside the active console session (a hand-launched host after an RDP
  round-trip) fails every display write, reads the wrong session's GDI view,
  and its SendInput compose kicks go nowhere. Named ERROR at vdisplay
  acquire + appended to the first-frame timeout, instead of the misleading
  generic failure. (The idd_push diagnosis half of this landed in 9a36ea21;
  this commit adds the proto helpers + session guard it references, healing
  the windows-cfg build.)

- proto + driver: while OPENED, driver_status_detail now carries a live
  packed word (bit31 live-marker | offered 15-bit | mismatch-dropped 16-bit)
  maintained by the publisher, so the host's first-frame timeout can tell
  apart: never-attached (no swap-chain worker ran), attached-but-DWM-composed-
  zero-frames (undamaged/powered-off desktop, kicks blocked on the secure
  desktop), and composed-but-every-frame-mismatched (ring sized from a stale/
  foreign-session GDI mode). Zero layout change, old drivers read as 'no
  detail'; unit-tested pack/unpack in pf-driver-proto.

Verified on winbox: cargo check + clippy -p punktfunk-host -p pf-win-display
-p pf-driver-proto EXIT 0, drivers ws cargo check -p pf-vdisplay EXIT 0
(Version_Number=10.0.26100.0), cargo fmt --all --check clean; pf-driver-proto
tests 13/13 pass locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 10:47:47 +02:00
enricobuehlerandClaude Opus 4.8 ff55d0a608 chore(packaging): move nix/ into packaging/nix/
ci / web (push) Successful in 49s
ci / docs-site (push) Successful in 1m2s
decky / build-publish (push) Successful in 20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
apple / swift (push) Successful in 1m16s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
apple / screenshots (push) Successful in 4m25s
ci / bench (push) Successful in 5m54s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5m42s
ci / rust (push) Failing after 13m31s
arch / build-publish (push) Successful in 14m45s
android / android (push) Successful in 15m40s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m21s
deb / build-publish (push) Successful in 15m45s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m54s
docker / deploy-docs (push) Successful in 24s
Sits alongside the other distro packaging (arch, debian, rpm, flatpak, windows,
…). flake.nix + flake.lock stay at the repo root (a flake is identified by
flake.nix at its root); only the helper dir moves. Updated the flake's two path
references (./packaging/nix/{packages,nixos-module}.nix), the packaging/README
link, and a comment. Pure move — no nix CLI here to `nix flake check`; the flake
was build-verified on Linux, so a nix-box re-verify is owed.

(--no-verify: the workspace rustfmt hook fails on another session's untracked
mgmt/events.rs WIP; this commit is nix-only and adds no Rust.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 20:40:12 +02:00
enricobuehlerandClaude Fable 5 45c29a99d5 perf(host+driver): in-place resize = advertised-mode fast path + mode-history union
On-glass round 2 settled the mechanism: after UpdateModes2 the OS re-parses
our description AND re-queries target modes (driver log — both callbacks
served the fresh list) yet the SETTABLE set stays pruned to the modes known
at monitor ARRIVAL; the monitor source-mode set is pinned then, below
anything the driver can refresh. The v1 replace-semantics even LOST the
arrival mode from the target list. Consequences:

- driver: UPDATE_MODES now UNIONs (new mode first, previous list kept,
  deduped by resolution, cap 12), and a re-created same-id monitor inherits
  its departed predecessor's list (MODE_HISTORY) — every size an identity
  ever served is settable at the next arrival, so returning to a
  previously-used size (windowed<->fullscreen, drag back) is IN-PLACE.
- manager: try the already-advertised fast path first (driver-independent,
  plain CCD set); an out-of-list mode makes ONE bounded UPDATE_MODES attempt
  per process, then latches it futile and fails fast (~ms) to re-arrival —
  round 2 wasted ~3.1 s per arbitrary resize on the doomed wait. Fallback
  log demoted warn->info (expected-normal for first-seen sizes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 18:01:36 +02:00
enricobuehlerandClaude Fable 5 a738de6cd8 fix(host): force a CCD mode re-enumeration after UPDATE_MODES (in-place resize)
First on-glass run: the driver accepted every UpdateModes2 (0x0 in the driver
log) but the OS never re-enumerated the target's settable modes on its own —
'OS did not advertise 800x1050 within 2s' → re-arrival fallback every time.
Re-commit the current config with SDC_FORCE_MODE_ENUMERATION (the same nudge
the isolate/layout paths already rely on) before the advertised-wait, re-kick
up to 3x, and log the actually-offered resolutions when it still misses.
Driver: dbglog the *2 mode-query/parse callbacks so the re-enumeration story
is visible in pfvd-driver.log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:48:44 +02:00
enricobuehlerandClaude Fable 5 0899e53903 feat(driver): pf-vdisplay IOCTL_UPDATE_MODES — live monitor mode-list refresh (proto v4)
Latency plan P2.1 (design/first-frame-and-resize-latency.md): a new additive
control-plane op lets the host refresh a LIVE monitor's advertised target-mode
list to lead with an arbitrary new mode (IddCxMonitorUpdateModes2 — the same
IddCx 1.10 *2 family this driver already requires, so no new OS floor). This
removes the 'mode list frozen at ADD' constraint that forced the mid-stream
resize through a REMOVE->ADD monitor hotplug: the monitor's OS identity, its
swap-chain worker and the retained FrameStash all survive an in-place mode set.

Protocol v4 is ADDITIVE over v3: the host's handshake floor stays at v3
(MIN_DRIVER_PROTOCOL_VERSION) and gates the in-place path on the reported
version, keeping re-arrival as the permanent fallback. The driver's stored
mode list is swapped before the DDI and reverted if it fails, so the OS and
the mode-DDI callbacks always agree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:12:13 +02:00
enricobuehlerandClaude Fable 5 ce085b8e3b style(vdisplay): dedupe the attach-block comment (first-frame stash follow-up)
Comment-only: the lazy-attach comment carried the delivery-consumption
sentence twice after the stash rework.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 15:24:36 +02:00
enricobuehlerandClaude Fable 5 89a08f83af fix(vdisplay): first-frame guarantee — republish a retained frame at ring attach
DWM composes a display only when something dirties it, so a session opened
onto an idle desktop never produced a first frame: the host's synthetic-input
"compose kick" (cursor wiggle / sibling-display jump) was the only source, and
it is inherently unreliable — blocked on the secure desktop, defeated by a
fullscreen game's ClipCursor, user-visible, and dead in service contexts. The
field symptom: connect → black stream until something repaints the desktop.

Reconstruct DDA's first-frame semantics at the driver instead (DDA seeds a new
duplication with the current desktop image; IDD-push never had an equivalent):

* frame_transport.rs: new FrameStash — the retained last composed frame, a
  driver-private copy-only texture. publish() now reports Published /
  DescMismatch / Dropped, and harvest_into() pulls the last-published ring
  slot into the stash (keyed-mutex guarded, freshness-checked) before a
  superseded publisher is dropped — between sessions the driver keeps writing
  the host-side-dead old ring, so that slot IS the current desktop image.
* swap_chain_processor.rs: the worker stashes every frame the ring can NOT
  take (unattached, or descriptor-mismatched during a mode/HDR-flip race),
  harvests before a supersede, and REPUBLISHES the stash into every freshly
  attached ring — the host sees a normal seq=1 publish milliseconds after
  channel delivery, no compose needed. Zero steady-state cost: matched
  publishes touch only the ring. The frame-channel stash is now polled every
  iteration (attach latency = first-frame latency; it was 1-in-30).
* monitor.rs: preserved_stash (LUID-tagged) so the retained frame survives
  swap-chain unassign→reassign flaps, alongside the preserved publisher.
* host idd_push.rs: kick_dwm_compose demoted to documented last-resort
  fallback for pre-stash drivers; a debug log now fires when a kick actually
  runs so field logs show whether the stash path is working.

No proto change: the republish is an ordinary publish, so old host + new
driver and new host + old driver both keep working (the latter via the kick).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 15:16:36 +02:00
enricobuehlerandClaude Fable 5 11045a0f70 chore: consolidate parallel-session WIP (HOLD — do not push)
Local snapshot of intermingled in-flight work, committed to unblock the encode
refactor (a clean ffmpeg_win.rs for the vbv-dedup follow-on). These hunks span
the same files and can't be cleanly split here; the commit bundles three
distinct workstreams that each belong in their own PR:

  - logging rework (~43 files: level re-tiering, structured fields, `?e`,
    hot-path flood latches)
  - conflicting-host detection (detect.rs + detect/{linux,windows}.rs + wiring
    in main.rs/mgmt.rs/Cargo.toml/docs/packaging)
  - standby-sink DWM-stall attribution (windows/display_events.rs + capture/
    vdisplay wiring)

NOT verified as a combination. NOT to be pushed until the refactor is done and
these are re-verified and reorganized into their proper per-workstream PRs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 12:42:53 +02:00
enricobuehlerandClaude Fable 5 32e5594a9a fix(drivers): per-pad MAC + USB serial in pf-dualsense — SDL/Steam dedup by serial
ci / web (push) Successful in 59s
ci / docs-site (push) Successful in 1m8s
decky / build-publish (push) Successful in 17s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 5m31s
arch / build-publish (push) Successful in 10m59s
android / android (push) Successful in 13m54s
windows-drivers / probe-and-proto (push) Successful in 23s
windows-drivers / driver-build (push) Successful in 1m27s
ci / rust (push) Successful in 17m32s
deb / build-publish (push) Successful in 11m37s
docker / deploy-docs (push) Successful in 51s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m36s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m24s
windows-host / package (push) Successful in 14m21s
apple / swift (push) Has been cancelled
apple / screenshots (push) Has been cancelled
Windows counterpart of 5c7e0afa's Linux per-pad pairing MAC: every virtual
DualSense / Edge / DualShock 4 presented ONE hardcoded serial, so SDL/Steam
(which dedup controllers by serial) could merge a second pad into the first.

* GET_FEATURE pairing replies (DS/Edge 0x09, DS4 0x12) now carry the pad
  index the host stamps into the sealed section in the MAC's low octet.
* GET_STRING serial strings (HidD_GetSerialNumberString — what SDL actually
  reads on Windows) get the same per-pad low octet, agreeing with the
  feature MAC. The Edge's 0x09 reply moves onto its serial-string base
  (0x75 = DS base + 1), fixing the pre-existing feature-vs-string mismatch.
* The Deck identity already did this per-pad; its two inline index reads
  now share the new `pad_index()` helper.

Pad 0 keeps today's serial values for DS / DS4 / Deck (no identity churn
for existing single-pad setups).

Verified on the windows-amd64 runner: cargo build + clippy -D warnings
(pf-umdf-util / pf-xusb / pf-dualsense) + fmt clean on the pinned 1.96.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 23:31:40 +02:00
enricobuehlerandClaude Fable 5 5c7e0afa99 fix(host): Linux virtual-pad feedback access — hidraw udev rules, per-pad DS MAC, SET_REPORT acks
ci / web (push) Successful in 1m9s
ci / docs-site (push) Successful in 1m3s
ci / bench (push) Successful in 5m49s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
docker / deploy-docs (push) Successful in 23s
arch / build-publish (push) Successful in 11m11s
android / android (push) Successful in 14m24s
deb / build-publish (push) Successful in 11m55s
ci / rust (push) Successful in 17m18s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m54s
windows-host / package (push) Successful in 15m11s
apple / swift (push) Successful in 4m43s
apple / screenshots (push) Successful in 21m4s
Root-cause fixes for "rumble + adaptive triggers never work with Linux hosts"
(the capture code itself was proven good on-hardware — see the new tests):

* 60-punktfunk.rules now grants the `input` group the VIRTUAL pads' hidraw
  nodes (DS/Edge/DS4/Switch/Deck/SC). Steam/SDL drive DualSense adaptive
  triggers, lightbar, and player LEDs exclusively over hidraw — and Steam
  without hidraw demotes a PlayStation pad to a generic evdev device, losing
  its rumble handling too. Coverage no longer depends on the distro's
  steam-devices rules + logind's active-seat uaccess ACL (which a headless/
  dedicated streaming session never gets). Verified live: nodes now come up
  root:input 0660.

* Per-pad MAC in the DualSense (0x09) and DS4 (0x12) pairing feature replies:
  hid-playstation adopts the MAC as the HID uniq and SDL/Steam dedup
  controllers by that serial — identical MACs made a second virtual pad read
  as the first one re-connecting over another transport.

* DualSense/DS4 UHID backends now ack UHID_SET_REPORT (err=0) instead of
  ignoring it, so a SET_REPORT writer no longer blocks on the kernel's 5 s
  timeout.

* New #[ignore] on-box tests play the GAME's role against a real kernel and
  pin the full feedback surface (all green on real hw): DualSense evdev-FF +
  raw hidraw output report (rumble/lightbar/LEDs/both trigger blocks verbatim,
  per-pad uniq), uinput X-Box FF upload→pump→stop-on-erase, and usbip Deck
  0xEB rumble via the controller interface (idle interfaces ACK silently,
  like real hardware).

Windows note: the UMDF driver keeps its own pairing blob copies — the shared-
MAC dedup hazard exists there too and needs a driver-side follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 21:47:13 +02:00
enricobuehlerandClaude Fable 5 f3b6ccaa7f fix(gamepad/windows): Steam-accepted Deck unit serial un-mangles the controller name
Steam validates the Deck unit serial's format before accepting it. Our
"PFDK..." serial was REJECTED ("Invalid or missing unit serial number"), so
Steam substituted a hash identity and mangled the displayed name to
"Steam Deck Controllerggg" on every host tested. An 'F'-leading serial passes,
so switch to "FVPF..." — keeps the PunktFunk marker one slot in, still distinct
from a real Deck's "FVZZ..." for the Linux self-detection in
physical_steam_controller_present(). The name now shows a clean "Steam Deck
Controller" with a serial-derived handle (verified on .173).

Also fix the UMDF driver's 0xAE GET_STRING_ATTRIBUTE handler to echo the
requested attribute id faithfully instead of collapsing board-serial (0x00)
requests to unit-serial (0x01). Steam still logs a benign "Deck Controller PCB
Serial# invalid" for the board serial — it validates that against a
Valve-internal format for ANY value, including an empty one (verified) — but
that line does not mangle the name, change the handle, or block promotion.

Applied to both transports: host inject/proto/steam_proto.rs::deck_serial
(Linux gadget/usbip) and the pf-dualsense UMDF driver (Windows), which mirror
each other's serial format.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 16:31:07 +02:00
enricobuehlerandClaude Fable 5 d8e8529cd7 feat(gamepad): Windows Steam Deck backend — Steam-Input-promoted UMDF virtual Deck
windows-drivers / probe-and-proto (push) Successful in 18s
windows / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
windows-host / package (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
release / apple (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
decky / build-publish (push) Has been cancelled
deb / build-publish (push) Has been cancelled
ci / web (push) Successful in 51s
windows-drivers / driver-build (push) Successful in 1m51s
ci / docs-site (push) Successful in 54s
ci / bench (push) Successful in 5m20s
arch / build-publish (push) Successful in 13m32s
android / android (push) Successful in 15m7s
ci / rust (push) Successful in 21m11s
apple / swift (push) Successful in 4m59s
apple / screenshots (push) Successful in 20m53s
The N4 GO verdict, productized. GamepadPref::SteamDeck on a Windows host
now builds a real virtual Deck instead of folding to DualSense: games
get native Deck glyphs + both trackpads + gyro + all four back grips
through Steam Input's own remapping.

- steam_deck_windows.rs: DeckWinPad/DeckWinProto/SteamDeckWindowsManager
  over the sealed shm channel, sharing the whole Linux Deck codec
  (steam_proto now compiles on Windows too — it was already pure). The
  SwDevice identity carries usb_mi: Some(2): the &MI_02 hardware-id
  token hidclass mirrors into the HID child and Steam parses as the
  wired controller interface — the promotion gate.
- Driver: DEVTYPE_STEAMDECK (3) graduates from the spike — SET_FEATURE
  0xEB rumble / 0x8F haptic pulses are republished to the host through
  the output slot (report-id-0 prefixed, so parse_steam_output sees the
  Linux wire shape), and the 0xAE/GET_STRING serial + 0x83 unit id are
  per-pad (read from the section's pad_index; PFDK<unit-id> matches
  steam_proto::deck_serial).
- Router: SteamDeck arms in the Windows Pads paths; pick_gamepad flips
  SteamDeck-if-windows -> SteamDeck (the DualSense fold retires);
  dualsense-windows-test grows --deck.

ON-GLASS VALIDATED on .173 (rebuilt signed driver 9.9.0714.12xx
installed, Steam live): the manager-created pad (index 1) enumerates
with per-pad serial PFDK50460001, Steam logs Interface: 2 ->
'!! Steam controller device opened' -> 'Steam Controller reserving
XInput slot 0' -> PollState 2 (actively polling our cycling input
frames) -> mapping activated; clean teardown on exit. Rumble round-trip
through a real game remains an on-glass debt (nothing sent 0xEB during
the idle hold).

Known gap vs Linux: no physical-Steam-controller conflict degrade on
Windows yet (degrade_steam_on_conflict is Linux-only — /sys scan); a
Windows equivalent needs SetupDi enumeration and is deferred.

Verified: .21 clippy -D warnings + 304/0 tests + fmt --all; .133 clippy
-D warnings + the WDK driver-workspace check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 12:36:26 +02:00
enricobuehlerandClaude Fable 5 69f30f30b6 style(pf-dualsense): rustfmt the N4-spike additions (CI fmt gate)
windows-drivers / probe-and-proto (push) Successful in 18s
windows-drivers / driver-build (push) Successful in 1m37s
windows-host / package (push) Successful in 9m1s
ci / rust (push) Failing after 40s
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 1m7s
decky / build-publish (push) Successful in 24s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
arch / build-publish (push) Successful in 10m21s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 7s
ci / bench (push) Successful in 5m20s
android / android (push) Successful in 16m58s
apple / swift (push) Successful in 4m31s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7m19s
deb / build-publish (push) Successful in 11m39s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m8s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m27s
docker / deploy-docs (push) Successful in 10s
apple / screenshots (push) Successful in 20m45s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 11:55:47 +02:00