Commit Graph
2782 Commits
Author SHA1 Message Date
enricobuehler 608baf63be Merge pull request 'Post-sleep sessions still failed on 0.25.0 — the host was holding open the very device its recovery asks PnP to cycle' (#119) from worktree-vdisplay-reap-pnputil into main
apple / swift (push) Successful in 1m40s
ci / web (push) Successful in 1m21s
ci / rust-arm64 (push) Successful in 2m46s
ci / docs-site (push) Successful in 1m20s
ci / bun-nix (push) Successful in 26s
android / android (push) Successful in 6m37s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 12s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 7s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 7s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 12s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
apple / screenshots (push) Successful in 6m8s
deb / build-publish-host (push) Successful in 4m27s
deb / build-publish-client-arm64 (push) Successful in 2m0s
deb / build-publish (push) Successful in 5m36s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m1s
docker / builders-arm64cross (push) Successful in 8s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m23s
ci / rust (push) Successful in 9m44s
docker / deploy-docs (push) Successful in 36s
arch / build-publish (push) Successful in 12m2s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 4m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 4m14s
windows-host / package (push) Canceled after 11m59s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
Reviewed-on: #119
2026-08-08 13:29:09 +00:00
enricobuehler 8f9c72877e Merge pull request 'An SDK fix could never reach an installed plugin — the runner now carries it' (#117) from worktree-runner-sdk-reconcile into main
ci / bun-nix (push) Successful in 24s
ci / web (push) Successful in 1m9s
ci / docs-site (push) Successful in 1m16s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 15s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 12s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 12s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 18s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 13s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 41s
ci / rust-arm64 (push) Successful in 2m28s
deb / build-publish-client-arm64 (push) Successful in 1m35s
deb / build-publish-host (push) Successful in 4m13s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 2m59s
sdk-publish / publish (push) Successful in 1m52s
docker / builders-arm64cross (push) Successful in 15s
deb / build-publish (push) Successful in 7m16s
ci / rust (push) Successful in 6m52s
arch / build-publish (push) Successful in 7m29s
docker / deploy-docs (push) Failing after 3m54s
windows-host / package (push) Successful in 15m55s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 25s
nix / flake (push) Successful in 14m35s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m52s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 21m11s
Reviewed-on: #117
sdk-v0.1.4
2026-08-08 12:41:58 +00:00
enricobuehler 8f32976349 Merge pull request 'Library source settings never opened — the drawer still called the old plugin origin' (#118) from worktree-library-settings-origin-split into main
ci / bun-nix (push) Successful in 31s
arch / build-publish (push) Canceled after 1m8s
ci / rust (push) Canceled after 52s
ci / docs-site (push) Canceled after 1m12s
ci / rust-arm64 (push) Canceled after 1m15s
ci / web (push) Canceled after 1m15s
deb / build-publish (push) Canceled after 55s
deb / build-publish-host (push) Canceled after 45s
deb / build-publish-client-arm64 (push) Canceled after 8s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 14s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 19s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 4s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 4s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 17s
docker / builders-arm64cross (push) Canceled after 0s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 13s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 26s
windows-host / package (push) Canceled after 1m54s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 53s
Reviewed-on: #118
2026-08-08 12:40:44 +00:00
enricobuehler deef5e4382 fix(console): library source settings 404'd — the drawer still called the old plugin origin
ci / bun-nix (pull_request) Successful in 35s
ci / web (pull_request) Successful in 1m19s
ci / docs-site (pull_request) Successful in 1m18s
ci / rust-arm64 (pull_request) Successful in 3m22s
ci / rust (pull_request) Successful in 4m30s
Opening a library source's settings did nothing, for every library plugin. Confirmed on
`.21` against the running console:

    console origin :47992  /plugin-ui/lutris/__config -> 404
    plugin  origin :47993  /plugin-ui/lutris/__config -> 401

The drawer fetches a RELATIVE `/plugin-ui/<id>/__config`, so it resolves against the
console's own origin — where `middleware/auth.ts` answers 404 for `/plugin-ui/**`
unconditionally and by design. That refusal is the 2026-08-05 review's origin split
(H-3): plugin UIs moved to their own listener, and neither origin may serve the other's
paths. The drawer is the only consumer of `/plugin-ui` that is NOT an iframe — every
other caller builds an absolute URL from `pluginOriginFrom(uiConfig)` — so it was the
one thing the split broke, and nothing failed loudly enough to notice.

The fix is deliberately not to point the drawer at the plugin origin. That needs CORS
plus cross-site cookies, and it would put a plugin-controlled response inside a
credentialed cross-origin fetch — reopening exactly the hole the split closed. What
this drawer needs is DATA, not an embedded UI: `/api/plugin-config/<id>` reads the
plugin's `__config` server-side over loopback and returns the JSON same-origin, so no
plugin markup or script is ever served from the console origin and the per-boot secret
stays on the server, as with the `/plugin-ui` proxy.

`/api/**` is always session-gated (`isPublicPath`), so the new route inherits the gate
and answers 401 as JSON rather than redirecting to /login — which is what a `fetch`
needs and what the old path could never give it. It forwards only GET and PUT, reads
the body BEFORE the stale-credential retry (`readRawBody` drains the stream, so a
retried PUT would have saved `{}` over the operator's config), and passes the plugin's
own body through untouched so a 400's decode issue still reaches the operator.

Verified against the real built server: `/api/plugin-config/lutris` answers 401 — the
route resolves and is gated, and the BFF catch-all at `api/[...]` does not swallow it —
while `/plugin-ui/lutris/__config` still answers 404 on the console origin, i.e. the
split is intact. `/api/v1/status` still reaches the BFF. tsc clean, production build
clean, i18n 633 messages across en+de, biome clean on both touched files (the one
warning in SourceSettings.tsx pre-dates this change).
2026-08-08 14:17:58 +02:00
enricobuehler 32cc8dd529 fix(runner): an SDK fix could never reach an installed plugin
ci / bun-nix (pull_request) Successful in 21s
ci / web (pull_request) Successful in 1m17s
ci / docs-site (pull_request) Successful in 1m29s
ci / rust-arm64 (pull_request) Successful in 3m5s
ci / rust (pull_request) Successful in 4m31s
nix / flake (pull_request) Failing after 11m38s
Publishing `@punktfunk/host@0.1.3` — the release that lets a library scanner register
`category`, so Lutris and Heroic stay out of the console nav — reached **no existing
install**. Measured on `.21`: the only thing that moved it was deleting `bun.lock` by
hand over ssh. A fix that needs an ssh session is not a fix.

**Why nothing reached it.** Every plugin resolves the SDK from the plugins tree, and
`bun.lock` pins it to an exact version with an integrity hash. Nothing in any
user-facing flow re-resolves that pin: installing a plugin, reinstalling it, and even
updating it to a newer release all leave the SDK alone, because the plugin's `^0.1.x`
range is already satisfied by what is locked. `bun update` does not help either — the
plugins are pinned exactly in the root manifest, so there is no direct dependency to
update through.

**Where the fix belongs.** The runner. It is bundled from this same `sdk/` at the
host's release commit (`packaging/arch/PKGBUILD` builds `src/runner-cli.ts` into the
punktfunk-scripting package), so `SDK_VERSION` is by construction the SDK matching the
host now on disk. A host upgrade is therefore the one moment that can carry an SDK fix
to already-installed plugins, and now it does — before any plugin loads, and with no
operator action at all.

**Why it re-resolves the whole lockfile** rather than pinning the SDK at the root: a
targeted `bun add @punktfunk/host@<v>` does NOT work while plugins declare the SDK in
their own `dependencies` (all six scanners do, though none import it). bun honours
their locked resolution and gives each a private nested copy that then SHADOWS the
root — measured, 5 nested copies, which is how I first "fixed" the box while leaving
every plugin still importing 0.1.2. A lockless resolve hoists one copy for everyone.
Once the plugins drop that spurious dependency this can become the targeted form.

Safety, because this runs unattended at boot on a tree the operator's plugins load
from: plugin versions are pinned exactly in the root manifest so a re-resolve cannot
move them (verified — lutris stays 0.1.0); the lockfile is backed up and restored if
the install fails or fails to deliver; and every failure is logged and swallowed, so a
dependency refresh can never stop working plugins from starting. The no-op path is the
one that runs on every healthy box, so it is tested first: same version, or no SDK at
all, touches nothing and logs nothing.

The SDK is bumped to 0.1.4 because its published content changed. Republishing 0.1.3
is impossible, and letting source drift from a published version is precisely the
defect that produced this whole chain — 0.1.2 was published before it forwarded
`category`, then the source changed underneath it without a bump. `version.test.ts`
fails if `SDK_VERSION` and `package.json` ever disagree.

Verified end to end on `.21` against a tree seeded from the operator's real pre-fix
backup: 0.1.2 → 0.1.3 automatically, one hoisted copy, no nested copies, plugin
versions preserved, and a second run is a silent no-op. SDK 79 tests pass (5 new),
typecheck clean.
2026-08-08 14:07:11 +02:00
enricobuehler fba22c6c64 fix(host/vdisplay): the host no longer vetoes its own wake-from-sleep recovery — control handles close on retire
ci / bun-nix (pull_request) Successful in 21s
ci / docs-site (pull_request) Successful in 1m23s
ci / web (pull_request) Successful in 1m29s
apple / swift (pull_request) Successful in 1m39s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 2m19s
android / android (pull_request) Successful in 8m16s
ci / rust (pull_request) Successful in 9m59s
The control-device sharing contract was 'bare HANDLE copies, never
closed for the process lifetime': retired handles were deliberately kept
alive because the pinger/linger threads and the capture delivery
closures held raw copies whose soundness depended on no-close. The cost
surfaced in the 2026-08-08 field log: after a wake left the driver
hostless, every adapter reload came back REFUSED (Generic failure) —
and an open control handle is exactly what vetoes the PnP disable (and
can wedge the pnputil restart) the recovery leans on.
reset-pf-vdisplay.ps1 stops the whole host service precisely to get
those handles closed; the in-process recovery could not, because the
process could never close them.

Ownership is now Arc all the way out: ensure_device/device_handle/
control_device_handle hand out Arc<OwnedHandle> clones, every consumer
holds its clone across its IOCTLs (the capture closures each own one —
Arc<OwnedHandle> is Send+Sync, ending the isize smuggling), and
retiring drops only the manager's reference, so the handle CLOSES when
the last in-flight user drains. DeviceSlot::retired is gone. The
recovery path now releases the manager's reference at the first absent
sighting — the 3 s ABSENT_SETTLE doubles as the drain window — and
again before a not-ready-deadline reload, so the PnP cycle finally runs
against a device the host is no longer holding open.

The driver attaches no meaning to the control file closing (host-gone
is the IOCTL-liveness watchdog, EvtFileClose deliberately unhooked), so
the close has no driver-side side effects. Lock order note: RECOVERY →
device is now taken (the release hooks); the forbidden inverse still
never occurs — VdisplayDriver::open never reloads.
2026-08-08 13:37:05 +02:00
enricobuehler 4b514cc07c Merge pull request 'An OLED palette, and split WHETHER the gamepad UI is offered from WHEN it appears' (#116) from worktree-oled-theme-gamepad-ui-split into main
apple / swift (push) Successful in 1m33s
ci / rust-arm64 (push) Successful in 2m51s
ci / web (push) Successful in 3m13s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m45s
ci / bun-nix (push) Successful in 45s
ci / docs-site (push) Successful in 1m30s
ci / rust (push) Successful in 4m55s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 23s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 40s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 1m3s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m51s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 1m2s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 40s
deb / build-publish-client-arm64 (push) Successful in 2m42s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 13s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m17s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 47s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m18s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m17s
deb / build-publish-host (push) Successful in 6m16s
docker / builders-arm64cross (push) Successful in 11s
release / apple (push) Successful in 9m52s
docker / deploy-docs (push) Successful in 36s
android / android (push) Successful in 13m40s
deb / build-publish (push) Successful in 9m17s
arch / build-publish (push) Successful in 14m36s
flatpak / build-publish (push) Successful in 7m20s
apple / screenshots (push) Successful in 6m0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m15s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 21m21s
Reviewed-on: #116
2026-08-08 11:22:34 +00:00
enricobuehler f242b2d2fc fix(host/vdisplay): a refused adapter reload now says WHY, and never targets a phantom devnode
Field log 2026-08-08 (0.25.0, wake from sleep): every session died on
'the adapter devnode could not be reloaded (Generic failure)' — the WMI
catch-all — because the REFUSED branch reported only the Disable
exception and threw away everything that would identify the failure
mode: why the pnputil /restart-device fallback ALSO failed (its exit
code — 3010 'needs a reboot' is its own diagnosis), what state the
devnode was in, and whether the right devnode was even targeted.

That last one is a real trap, not just missing telemetry: Get-PnpDevice
lists not-present PHANTOM devnodes (upgrade/reinstall leftovers), and
Select-Object -First 1 could hand every recovery attempt a phantom —
whose disable and restart both fail exactly like the field log — while
a live node sat unexamined. The selector now prefers present nodes (OK
before problem-state), and a phantom-only state gets a truthful
refusal: no reload can revive a devnode record whose device is gone;
only reinstalling re-creates it.

The REFUSED line now carries devnode counts, the chosen node's PnP
Status + ConfigManager problem code, and the restart exit code, so the
next field log decides between handle-veto, phantom, and problem-state
instead of reading 'Generic failure'. Decode pinned by test.
2026-08-08 13:22:05 +02:00
enricobuehler 27ceab2f6c Merge pull request 'The SDK could not be published at all — bun publish runs prepare, and prepare needs bun2nix' (#115) from worktree-sdk-publish-prepare-hook into main
ci / bun-nix (push) Successful in 43s
ci / web (push) Successful in 1m23s
ci / docs-site (push) Successful in 2m13s
ci / rust (push) Successful in 5m9s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 9s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 7s
ci / rust-arm64 (push) Successful in 5m35s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 7s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 24s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 29s
deb / build-publish-client-arm64 (push) Successful in 3m17s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 18s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 14s
arch / build-publish (push) Successful in 7m39s
deb / build-publish-host (push) Successful in 6m34s
docker / builders-arm64cross (push) Successful in 7s
sdk-publish / publish (push) Successful in 1m10s
deb / build-publish (push) Successful in 7m36s
docker / deploy-docs (push) Failing after 1m45s
plugin-kit-publish / publish (push) Successful in 1m5s
windows-host / package (push) Successful in 15m11s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 28s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 11m41s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 11m27s
nix / flake (push) Failing after 16m48s
Reviewed-on: #115
plugin-kit-v0.3.3 sdk-v0.1.3
2026-08-08 11:04:33 +00:00
enricobuehler 30bd10e301 feat(clients): an OLED palette, and split WHETHER the gamepad UI is offered from WHEN it appears
apple / swift (pull_request) Successful in 1m33s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 1m39s
ci / rust-arm64 (pull_request) Successful in 4m7s
android / android (pull_request) Successful in 5m1s
ci / docs-site (pull_request) Successful in 1m47s
ci / bun-nix (pull_request) Successful in 42s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m19s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m23s
ci / rust (pull_request) Successful in 14m32s
Four changes to the client interface, kept together because two of them touch the same rows
and the last is a bug the first would have made far more visible.

A thirteenth `ui_palette` entry, `oled`. The palette table is hand-mirrored in three languages
(`pf-console-ui`'s `library.rs`, `GamepadPalette.swift`, `GamepadPalette.kt`), so it goes into
all three at index 1, directly after the brand default — which keeps `PALETTES[0]` the unknown-id
fallback and keeps the dark-to-pale cycling order intact. What earns the name is arithmetic, not
a darker shade of violet: the ramp's first two stops are literally (0,0,0) and the ground is pure
black, so the shaded half of the field is pixels switched off rather than "very dark grey", and
the calm mix the form screens sit under lifts toward nothing at all. Mean cell luminance is 0.019
against Violet's 0.254. The bright corner keeps a faint indigo-to-violet ember so the backdrop is
still a field with somewhere to go, and that ember carries enough chroma at that luminance
(60 degrees of hue travel across 13 of the 16 cells) to satisfy the existing multi-tone assertion
without adding `oled` to the near-neutral exemption Graphite and Opal take. Each port gains an
`oled_is_actually_black` test that measures the claim — pure-black corner cells, a mean under half
the darkest other field's — rather than restating the table.

A new device key, `gamepad_ui_mode`. The gamepad-UI switch had been deciding two things at once:
whether to offer the controller-optimized interface at all, and that it appears only while a pad
is attached. A user asked for the second half to stop applying. `"connected"` (the default, and
exactly what the lone Bool meant) and `"always"` separate them, surfaced as a "Show it" row
directly under the switch on all five settings surfaces and built only while that switch is on —
a picker whose every option decides nothing is worse than no picker. `GamepadUIEnvironment.isActive`
takes the mode with NO default argument on purpose: a call site that forgot it would silently
strand everyone who chose Always back on "only with a controller", which is the one bug this
parameter exists to make impossible. An unrecognized value waits for a controller, so a mode a
newer client wrote can never trap an older one in a layout it has no way back out of. It stays a
device preference on both platforms, never part of a profile: which interface this device wears
has nothing to do with how a host streams to it.

The smoothness buffer is hidden under Lowest latency, not dimmed. Everywhere else already hid it
— the GTK and WinUI shells, the Apple touch and tvOS screens, the Android touch screen — because
under that intent it names a quantity that does not exist. Two surfaces disagreed: Apple's gamepad
settings screen left the row live and steppable, and the desktop console dimmed it, having no way
to drop a row from a fixed list. That list is now rebuilt each frame through a `row_applies`
filter. The concern about a vanishing row moving everything under the cursor does not apply here
and the new test says why: the row it drops sits directly BELOW the row that drops it, so the only
cursor that can be present when the list shrinks is the one on the intent row, which does not
move. Two latent hazards went with it — `apply_row` had been indexing the row list on the
assumption the cursor is always in range, and nothing re-clamped that cursor when another writer
changed the intent behind the screen's back.

Pale palettes were unreadable on tvOS, reported from the field. `GamepadInk` was never the
problem: it flips correctly for a pale field, it is not platform-gated, and every tvOS gamepad
entry point already published it. The cause is that this app sets `preferredColorScheme` nowhere
and declares no `UIUserInterfaceStyle`, so every SYSTEM-derived colour landing on those screens —
a `.secondary` placeholder, a `.bordered` button's chrome, a NavigationStack title, a material's
frost — resolved against the DEVICE appearance, which the palette cannot reach. On iPhone, iPad
and Mac a great many users sit in Light mode, so under a pale palette those colours came out dark
and the theme looked correct by accident; an Apple TV is Dark essentially always, so every one of
them rendered white on a light field. The mirror image was broken too and had simply never been
reported: a dark palette on a Light-mode iPhone was already drawing dark on dark. The scheme is
now published beside the ink, once, in `GamepadInkModifier`, because the two are halves of one
decision and publishing only the ink silently loses every colour the frameworks draw on the app's
behalf. Two structural amplifiers went with it: `ConsoleGlass` had been scoping the scheme to the
fill inside its `.background {}` on the tvOS and pre-26 branches while the 26 branch put it on the
content, so no console row's own content ever saw it on tvOS; and `LibraryView`'s navigation
chrome and its loading, error and empty states sit above `LibraryCoverflowView` and so were never
inked at all on tvOS and macOS, where that view is presented directly rather than through the
iOS-only `GamepadLibraryScreen` wrapper.

That last one exposed a second tvOS gap worth closing in the same breath: `ui_palette` had no row
in tvOS's ordinary Settings, and the gamepad settings screen that owns it everywhere else needs an
extended-profile controller to open on tvOS. An Apple TV driven by the Siri Remote alone could not
reach the palettes at all, which would now include the OLED one. `SettingsView.tvBody` carries a
Background row.

Verified: pf-console-ui builds, passes `clippy --all-targets -D warnings` and runs 74 tests clean
under linux/amd64 (a Mac `cargo check` of that crate is vacuous — every module is cfg'd to
linux/windows); `cargo fmt --check` clean for it and pf-client-core. Android `:app` runs 80 tests
with 0 failures, including four new `gamepadUiActive` cases and the palette parity table. The
Apple package builds for macOS AND tvOS and its 9 palette/gamepad-UI tests pass — the tvOS
typecheck is possible because the checked-in xcframework already carries a `tvos-arm64` slice. The
tvOS RENDERING fix is compile-verified only; an on-glass Apple TV check under a pale palette is
still owed, and is the one thing here that a build cannot answer.
2026-08-08 12:57:12 +02:00
enricobuehler 1df39d9617 fix(ci/sdk): the SDK could not be published at all — bun publish runs prepare
ci / web (pull_request) Successful in 1m10s
ci / rust-arm64 (pull_request) Successful in 2m28s
ci / docs-site (pull_request) Successful in 1m23s
ci / bun-nix (pull_request) Successful in 26s
ci / rust (pull_request) Successful in 4m45s
nix / flake (pull_request) Successful in 15m6s
`sdk-v0.1.3` failed at the publish step with `bun2nix: command not found`, exit 127.
Nothing was published, so 0.1.3 is still free.

`bun publish` runs the `prepare` lifecycle script, and sdk's `prepare` is
`bun2nix -o bun.nix` — regenerating the nix dependency file. That tool is a
devDependency of the repo, not something the `oven/bun:1` publish container has, and
the workflow's own install is `--ignore-scripts`, so nothing put it on PATH either.

This was latent, not new. `prepare` gained the bun2nix call on 2026-07-27 (1db8f763,
"move the bun packages to bun2nix"), while the last SDK publish was 0.1.2, bumped
2026-07-20. So the hook has been broken for every SDK release since it landed, and
0.1.3 is simply the first one to try. `@punktfunk/plugin-kit` has no `prepare` and was
never affected, which is why kit 0.3.2 published fine in that window and hid this.

The fix is NOT to copy `web/package.json`, which does the same job from `postinstall`.
That is right for web — it is never published — and would be worse here: a published
package's `postinstall` runs in every CONSUMER's install, so every plugin depending on
`@punktfunk/host` would try to run bun2nix and fail. `prepare` is the correct hook for
a published package (it does not run for consumers); it just must not assume a
repo-maintenance tool exists wherever a publish happens.

So the script skips when bun2nix is absent — and ONLY then. A present-but-failing
bun2nix still fails the script, because swallowing that would publish with a silently
stale bun.nix, which is the exact hand-maintained-hash problem 1db8f763 set out to end.
Both directions measured against the same `sh -e` bun and the Gitea runner use:
absent → exit 0, present-and-failing → exit 3.

`bun publish --dry-run` now completes and reports `+ @punktfunk/host@0.1.3`.
2026-08-08 12:55:37 +02:00
enricobuehler e4f8c64b9f Merge pull request 'Library scanners sat in the nav and could not sync local art — and you can now hide one game' (#113) from worktree-plugin-nav-category-and-art into main
audit / bun-audit (plugin-kit) (push) Successful in 19s
apple / swift (push) Successful in 1m38s
audit / bun-audit (sdk) (push) Successful in 48s
audit / pnpm-audit (push) Successful in 11s
audit / docs-site-audit (push) Successful in 1m8s
audit / bun-audit (web) (push) Failing after 1m14s
apple / screenshots (push) Successful in 5m46s
ci / rust-arm64 (push) Successful in 4m32s
audit / license-gate (push) Successful in 5m12s
ci / bun-nix (push) Successful in 38s
arch / build-publish (push) Successful in 8m1s
ci / docs-site (push) Successful in 1m12s
ci / web (push) Successful in 1m28s
android / android (push) Successful in 9m3s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 33s
deb / build-publish-client-arm64 (push) Successful in 1m25s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 27s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 28s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 12s
audit / cargo-audit (push) Failing after 10m5s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 27s
ci / rust (push) Successful in 7m55s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m31s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m22s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 8s
sdk-publish / publish (push) Failing after 31s
docker / builders-arm64cross (push) Successful in 11s
deb / build-publish-host (push) Successful in 4m20s
docker / deploy-docs (push) Successful in 35s
windows-host / package (push) Successful in 16m9s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 31s
deb / build-publish (push) Successful in 12m39s
nix / flake (push) Canceled after 14m7s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 14m17s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 13m18s
Reviewed-on: #113
2026-08-08 10:39:25 +00:00
enricobuehler 690ff7016b Merge pull request 'The config page missed the whole 0.25 env-var wave — jumbo frames and ten other knobs documented' (#114) from worktree-docs-config-page-0250-vars into main
ci / rust (push) Canceled after 11s
ci / rust-arm64 (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bun-nix (push) Canceled after 0s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 15s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 9s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Canceled after 0s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
Reviewed-on: #114
2026-08-08 10:37:14 +00:00
enricobuehler 6cffe29b13 feat(host,console): hide individual library titles
ci / web (pull_request) Successful in 1m13s
ci / docs-site (pull_request) Successful in 1m23s
apple / swift (pull_request) Successful in 1m40s
ci / bun-nix (pull_request) Successful in 21s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 2m28s
android / android (pull_request) Successful in 4m25s
ci / rust (pull_request) Successful in 6m26s
nix / flake (pull_request) Successful in 15m40s
The library had one visibility control and it was all-or-nothing: turn a SOURCE off
and every one of its games goes. There was no way to drop a single title — a Proton
tool the filter missed, a demo, a game someone doesn't want on the TV — short of
hiding the whole launcher it came from.

**Where the setting lives.** Not on the entry. Only manual custom entries are stored;
a scanner's and a plugin's titles are rebuilt from scratch on every scan and every
reconcile, so a flag written onto one would be erased by the next sync — silently, and
minutes later, which is the worst possible shape for a setting. So `library-hidden.json`
holds the ids, mirroring how `library-scanners.json` holds disabled sources. The id is
stable by construction (D2: a claimed store's entries keep `<store>:<external_id>`
across reconciles), so a hide survives a re-scan, a plugin restart, and a store's
built-in→plugin migration.

**Where it takes effect.** In `all_games`, which is the one place every play surface
already funnels through — the grid on a client, native clients, the GameStream app
list, and launch resolution. Putting it there rather than at each call site is
deliberate: a per-surface filter is a rule someone has to remember, and forgetting one
is precisely the class of bug the `file://` art asymmetry in the previous commit was.
Hiding is curation, not access control — nothing is deleted, and un-hiding is instant.

**The console is the one surface that still sees them**, or a hidden title could never
be brought back. That exception is a TYPE, not a flag: `GET /library` answers
`Vec<GameEntry>` on every lane but the operator's and `Vec<OperatorGameEntry>` on
theirs, so a hidden entry cannot reach a paired streaming client by someone forgetting
a filter — there is no field there to leak. `hidden` is skipped when false, so the
response is byte-identical to today's for a library with nothing hidden.

`PUT /library/hidden/{id}` is operator-only — neither the plugin lane nor a paired cert,
unlike the scanner toggle. A plugin has no business deciding what its operator sees, and
a client must not be able to hide a game on the host it is streaming from. The id is not
validated against the current library on purpose: a title can be legitimately absent at
that moment (launcher closed, plugin mid-sync, drive unmounted), and refusing the
operator's choice in that window is worse than storing an id that matches nothing today.

On the card, the poster dims and a Hidden badge says why — a faded tile with no label
reads as a broken cover. Its controls stay at full contrast and, unlike an ordinary
card's, are not hover-revealed: the un-hide button is the only way out of the state, and
hiding it behind a hover would strand anyone on a touch screen.

Verified on .21 (Linux): 469 host tests pass (5 new), clippy clean under `-D warnings`,
`cargo fmt --all --check` clean. The routing test is the one that earns its keep — every
library id contains a colon and Heroic's contain two, so a router that split on it would
404 the console against ids the host itself produced. Console: tsc clean, production
build clean, i18n 633 messages across en+de, biome clean on the touched files.
2026-08-08 12:33:54 +02:00
enricobuehler 44c87d7ac1 docs(site): configuration page catches up to 0.25 — jumbo frames and seven other missing knobs
ci / web (pull_request) Successful in 1m8s
ci / rust-arm64 (pull_request) Successful in 2m27s
ci / bun-nix (pull_request) Successful in 21s
ci / docs-site (pull_request) Successful in 1m17s
ci / rust (pull_request) Successful in 8m15s
The env-var reference had fallen behind the v0.25.0 CHANGELOG table. Added, with
the semantics taken from the code rather than the changelog one-liners:

- PUNKTFUNK_JUMBO / PUNKTFUNK_WIRE_MTU (Network & discovery), with a note
  explaining the ack-gated mid-session grow, the start-at-1500 behavior, the
  NIC/switch prerequisites, and the sub-1500 shrink direction of WIRE_MTU
- PUNKTFUNK_AUDIO_QUALITY / AUDIO_REDUNDANCY / AUDIO_OUTPUT_MODE — the legacy
  HOST_AUDIO / KEEP_DEFAULT rows are folded into the OUTPUT_MODE row as the
  aliases they now are (follow_default wins when both are set)
- PUNKTFUNK_NO_AUDIO_MINT (Windows minted-endpoint opt-out)
- PUNKTFUNK_PAD_AUDIO / PAD_AUDIO_SLOTS (Gamepads — DualSense speaker+haptics)
- PUNKTFUNK_NVENC_SPLIT_ARBITRATE (Advanced performance tuning)
- PUNKTFUNK_UI_PLUGIN_PORT / PUNKTFUNK_LIBRARY_ART_ROOTS (Auth, API & paths)
- PUNKTFUNK_VAAPI_DEVICE (client-side table)

Verified against the actual read sites (pf-host-config, wire_mtu.rs,
config.rs jumbo_wire_mtu, pad_audio.rs, minted.rs, art.rs, bun-https.mjs);
the page's remaining vars all still exist in code. MDX-compiles clean with GFM.
2026-08-08 12:31:25 +02:00
enricobuehler 975fef2048 fix(host/vdisplay): the ghost-monitor reap can no longer fail in silence
The reap that keeps departed virtual monitors from exhausting the IddCx
monitor-slot budget launched pnputil by BARE NAME — under the LocalSystem
service's PATH that can miss System32, SilentlyContinue swallowed the
miss, and the Rust side logged only when the count was positive: a reap
that removed nothing and a box with no ghosts were byte-identical
(silence). Ghosts then ratcheted up with every sleep cycle until
IOCTL_ADD wedged at 0x80070490 and every session black-screened — and
the wedge self-heal shipped in 0.25.0 retried an ADD behind a reap that
could never remove anything, which is exactly a persistent post-sleep
"no connection" surviving the b6acbd09 probe fix.

Same family and same cure as the adapter-reload path one function down:
resolve pnputil via $env:SystemRoot (a SYSTEM process must not trust
PATH anyway — a planted pnputil.exe would run elevated), pre-seed
$LASTEXITCODE to failure before every launch, and report found AND
removed unconditionally so "no ghosts" and "removed nothing" are
finally distinguishable in a field log. The report parse is split out
and pinned by tests like classify_reload_output.
2026-08-08 12:19:48 +02:00
enricobuehler 9089651406 Merge pull request 'The jitter ring only ever learned from clicks — it now grows on near-misses, un-does refused shrinks, and cashes growth on the click it already paid' (#111) from worktree-audio-jitter-lowwater into main
apple / swift (push) Successful in 1m36s
ci / web (push) Successful in 1m14s
ci / docs-site (push) Successful in 1m20s
ci / bun-nix (push) Successful in 1m59s
ci / rust-arm64 (push) Successful in 2m31s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 12s
ci / rust (push) Failing after 3m6s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 21s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 12s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 12s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 27s
deb / build-publish-client-arm64 (push) Successful in 1m55s
deb / build-publish-host (push) Successful in 4m38s
docker / builders-arm64cross (push) Successful in 9s
deb / build-publish (push) Successful in 5m5s
docker / deploy-docs (push) Successful in 32s
android / android (push) Successful in 10m35s
flatpak / build-publish (push) Successful in 7m16s
release / apple (push) Successful in 10m45s
windows-host / package (push) Successful in 12m21s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 25s
arch / build-publish (push) Successful in 13m42s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m35s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m50s
apple / screenshots (push) Successful in 6m9s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m19s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m15s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 23m55s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 24m41s
Reviewed-on: #111
2026-08-08 10:11:10 +00:00
enricobuehler 2a2427afc8 Merge pull request '"Native resolution" streamed the compositor's points, not the panel's pixels — and the window was never high-DPI either' (#112) from worktree-wayland-native-pixel-density into main
ci / bun-nix (push) Successful in 26s
ci / docs-site (push) Successful in 1m4s
android / android (push) Canceled after 1m19s
ci / web (push) Successful in 1m12s
apple / swift (push) Canceled after 1m24s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 1m31s
ci / rust (push) Canceled after 1m35s
ci / rust-arm64 (push) Canceled after 1m34s
deb / build-publish (push) Canceled after 1m15s
deb / build-publish-host (push) Canceled after 37s
deb / build-publish-client-arm64 (push) Canceled after 25s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 17s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 4s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Canceled after 0s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
flatpak / build-publish (push) Canceled after 4s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 7s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 1s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Canceled after 1m45s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Canceled after 0s
windows / build (aarch64-pc-windows-msvc) (push) Canceled after 0s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
Reviewed-on: #112
2026-08-08 10:09:55 +00:00
enricobuehler d237646c66 fix(host,sdk,kit): library scanners sat in the nav, could not sync local art, and so never got their settings
Three symptoms on .21, two defects. Lutris and Heroic appeared in the console sidebar
they explicitly opt out of; Lutris's settings were unreachable from the Library
screen; and Lutris and Steam logged `sync (startup) failed: HostRequestError`.

**The sidebar is a publish gap.** The console is correct — it keeps
`category: "library"` plugins out of the nav (`uiPlugins`, app-shell.tsx) — but the
host reports no category for them at all. `defineLibraryPlugin` sets it and
`sdk/src/ui.ts` forwards it; what SHIPS does not. `@punktfunk/host` was bumped to
0.1.2 on 2026-07-20 and `category` landed 2026-08-05 without a bump, so the registry's
0.1.2 is the pre-category build and every installed scanner registers without one.
Bumps the SDK to 0.1.3 — **inert until it is published**.

Because the field rides the untyped `pf.request` seam so an older host ignores it
rather than rejecting the registration, dropping it is silent by design. `serveUi` now
reads its own directory entry back and warns once when a requested category did not
land, the same way `defineLibraryPlugin` already warns when a store claim did not take.
That is what turns the next occurrence into a log line instead of a bug report.

**The missing settings and the failed sync are ONE defect: a write/read disagreement
about `file://`.** `local_art_bytes` decodes a `file://` value before testing
containment; `validate_art_paths` handed the raw value to `Path::new`, where
`file:///home/u/c.jpg` is a RELATIVE path whose first component is `file:`. It
canonicalized against the cwd, failed, and read as "outside every art root". So the
host refused every cover the kit's own `fileUrl` helper emits — the documented way for
a plugin to publish local art — while the read path would have served those same files.

That the two symptoms share a cause is not obvious and is why this is one commit: the
Library screen's settings control renders only for `origin: "plugin"`, and a source
becomes `plugin` only once it holds a store CLAIM, which is taken during a successful
reconcile. Lutris failed at entry 0 and Steam at entry 3, so neither ever claimed its
store, both stayed `origin: "builtin"`, and neither got a settings button. Heroic
reconciled (its art is http(s)) and has had its settings all along; rom-manager was
never affected because zero entries meant it never applied.

`art_path_is_servable` now decodes first, so both halves of the confinement judge the
same string. Confinement itself is unchanged: an out-of-root path is still refused in
`file://` clothing, which the test asserts alongside the accept case.

Diagnosing this took the HOST's journal, because both surfaces that should have
explained it lied. `HostRequestError` stringified to its bare tag, so the sync engine's
`${e.cause}` logged `HostRequestError` and discarded the method, the path and the
host's own message; it now renders all three, including an object-shaped cause that
used to print `[object Object]`. And the host logged "payload carries a field this lane
may not set" for BOTH refusals in `check_entry_fields`, so a 400 about an art path read
as an auth problem — it now logs the real reason and the entry title.

Verified on .21 (Linux): 463 host tests pass, clippy clean under `-D warnings`,
`cargo fmt --all --check` clean. The new art test fails without the fix and passes with
it. plugin-kit 71 and SDK 72 tests pass, both typecheck clean, biome clean.
2026-08-08 11:43:30 +02:00
enricobuehler 69728b6f4e fix(pf-presenter): "Native resolution" streamed the compositor's POINTS, not the panel's pixels
ci / bun-nix (pull_request) Successful in 23s
ci / web (pull_request) Successful in 1m11s
ci / docs-site (pull_request) Successful in 1m19s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m33s
apple / swift (pull_request) Successful in 1m33s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 3m25s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m22s
android / android (pull_request) Successful in 4m41s
ci / rust (pull_request) Successful in 6m52s
A CachyOS / KDE Plasma 6.7.4 Wayland client with its 2560x1600@165 laptop panel at
150 % scaling negotiated 1706x1066 for "Native resolution" and streamed a visibly
blurry image. Two independent defects, and they stack — which is why forcing the mode
to 2560x1600 by hand did not fully fix it either.

1. `SDL_GetDesktopDisplayMode` reports a mode in SCREEN COORDINATES and hands the
   pixels-per-point ratio back separately as `pixel_density`. We read `m.w`/`m.h` raw.
   KDE advertises that panel as 1707x1067 points with a density of ~1.4997,
   `render_scale::apply` even-floors both odd axes, and 1706x1066 goes on the wire —
   exactly the mode in the reporter's handshake log. Multiplying by the density
   recovers 2560x1600 to the pixel, because SDL derives it as the output's exact
   pixels/points ratio. On X11 and Windows SDL never sets a density and `SDL_video.c`
   normalizes the unset 0.0 to 1.0, so this is inert there: the bug needed a
   compositor doing FRACTIONAL scaling.

2. The SDL window was created without `HIGH_PIXEL_DENSITY`, so the Wayland surface
   stayed at buffer scale 1 — the Vulkan swapchain was built at 1707x1067 and KWin
   upscaled it to the glass. Even a correct 2560x1600 stream was resampled down and
   then back up. The same flaw silently shrank "Match window", which asks the host for
   `size_in_pixels()`. The reporter's `SDL_VIDEO_WAYLAND_SCALE_TO_DISPLAY=1` workaround
   is this same fix applied from outside SDL, which is why it helped.

The surrounding code was already written for pixels != points — the swapchain,
match-window and pointer mapping all read `size_in_pixels()` while window-size
persistence reads logical `size()` — so the flag only makes those two stop being the
same number. `display_scale()` starts reporting 1.5 into a swapchain that is 1.5x
larger, leaving the OSD the size it already was.

Also closes a smaller hole on the way past: only an `Err` from SDL reached the
1920x1080 fallback, so a display that reported a 0x0 mode sent a 0x0 request.

Verified on home-worker-5 (CachyOS — the reporter's distro, real SDL 3.4.14):
`cargo clippy --all-targets -p pf-presenter -- -D warnings` clean and 18/18
pf-presenter tests pass, three of them new and pinned to the field-reported numbers.
2026-08-08 11:15:58 +02:00
enricobuehler 3bb87d260e fix(audio): detect jitter before it is audible, and stop re-probing a depth the link just refused
apple / swift (pull_request) Successful in 1m38s
apple / screenshots (pull_request) Skipped
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 2m1s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m44s
ci / web (pull_request) Successful in 1m38s
android / android (pull_request) Successful in 4m52s
ci / docs-site (pull_request) Successful in 1m33s
ci / rust-arm64 (pull_request) Successful in 4m10s
ci / bun-nix (pull_request) Successful in 28s
ci / rust (pull_request) Successful in 9m26s
The 0.25.0 MacBook field report — audio jitter 'at certain points' — is the
jitter policy learning exclusively from audible failures, on both of its
sides. Growth needed THREE audible underruns before deepening the ring; the
A/V sync loop re-tested a shallower ring every five quiet seconds and paid an
audible starvation event every time it was wrong, forever; and a grown target
was never re-banked — growth raises a threshold, only a re-prime deepens the
ring — so a bunching link rode the knife edge, clicking once per bunching
period with the 'grown' target sitting inert. A ten-minute simulation of the
Wi-Fi power-save pattern (25 ms gaps / 300 ms, −50 ppm skew) measured ~2000
audible events under the shipped policy.

Three mechanisms, in JitterPolicy (Linux/Windows/Android) and mirrored in the
Swift AudioRing:

- NEAR-MISS: a read served with less than one protocol frame left over is the
  same evidence as an underrun, heard by no one. It grows the target one step
  per window, BEFORE the click — waiting for the third audible underrun means
  the user heard two.
- SHRINK PROBES: every shrink is armed for five seconds; answered by an
  underrun or near-miss it is undone on the spot, and a failed sync-driven
  shrink is not retried for a doubling backoff (60 s → 8 min). A probe that
  survives resets the backoff. Continuity outranks sync, now with a memory.
- HOLLOW RE-PRIME: an underrun while the depth AVERAGE runs more than a step
  below the target re-primes immediately, spending the click it already cost
  on the whole refill instead of limping. The average, not the instant, is
  what separates a hollow ring from one late packet, and it is seeded on
  prime so a fresh ring is never spuriously hollow.

Same simulation after: 9 audible events, tail clean but for the clock-skew
re-anchor (a genuinely slow host must re-bank every few minutes; only rate
adaptation would remove that, and no client has it). Neutralising the three
constants reproduces the ~2000 — the convergence tests fail against the old
behaviour.

Verified: 203 punktfunk-core tests, 254 Swift tests (5 skipped), clippy -D
warnings on punktfunk-core --all-features, cargo fmt --all --check.
2026-08-08 11:05:53 +02:00
enricobuehler be57587572 Merge pull request 'The release-rebuild prune called a helper that cannot exist in a release rebuild' (#110) from worktree-arch-rebuild-prune into main
apple / swift (push) Successful in 1m30s
ci / bun-nix (push) Successful in 19s
ci / web (push) Successful in 1m40s
ci / docs-site (push) Successful in 3m12s
ci / rust-arm64 (push) Successful in 6m8s
apple / screenshots (push) Successful in 6m19s
android / android (push) Successful in 10m12s
arch / build-publish (push) Successful in 9m5s
decky / build-publish (push) Successful in 55s
deb / build-publish-client-arm64 (push) Successful in 1m58s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 18s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 15s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 52s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 32s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 28s
deb / build-publish (push) Successful in 9m25s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m28s
deb / build-publish-host (push) Successful in 7m54s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3m3s
docker / builders-arm64cross (push) Successful in 14s
ci / rust (push) Successful in 17m40s
docker / deploy-docs (push) Failing after 3m46s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m52s
Reviewed-on: #110
2026-08-08 09:03:28 +00:00
enricobuehler 8f1c34c6bf fix(ci/arch): the release-rebuild prune called a helper that cannot exist there
apple / swift (pull_request) Successful in 1m32s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Failing after 1m51s
ci / web (pull_request) Successful in 1m25s
ci / bun-nix (pull_request) Successful in 52s
ci / docs-site (pull_request) Successful in 1m53s
android / android (pull_request) Successful in 6m47s
ci / rust (pull_request) Successful in 28m33s
The v0.25.0 rebuild published perfectly — registry has punktfunk-host 0.25.0-2 with
libavcodec.so=63-64, and it resolves on a real ffmpeg-9 box — then failed its last
step with

    prune_release_assets: command not found

`. scripts/ci/gitea-release.sh` sources from the CHECKED-OUT TREE, and a release
rebuild checks out the OLD TAG. So the step could only ever see the helpers that
existed when that tag was cut, and the prune is gated on exactly that path: the
helper was guaranteed absent in the only case that calls it. Adding it to a shared
script made it look available at review time while being unreachable at run time.

Only the workflow file is read from the dispatched ref, so the logic moves there,
inline. Same reasoning documented at both ends, including the corollary worth knowing
before the next rebuild: a PKGBUILD fix made after a tag does NOT reach a rebuild of
that tag either — the packaging comes from the tag too.

Verified by executing the one-liner's exact bytes out of arch.yml under /bin/sh (the
shell Gitea actually uses): keeps the new -2 set and gamescope, drops the superseded
-1 packages and their .sha256 sidecars, leaves other legs' .dmg/.deb untouched. The
`'\n'` survives the shell quoting, which was the part worth proving.

Also drops the now-dead helper from gitea-release.sh rather than leaving a function
no caller can reach, and leaves a warning there against the next one.
2026-08-08 10:57:49 +02:00
enricobuehler 1ef212a78d Merge pull request 'v0.25.0 shipped an Arch host no up-to-date box can install — and the pipeline had no way to tell' (#109) from worktree-arch-ffmpeg9-repackage into main
apple / swift (push) Successful in 1m39s
ci / rust-arm64 (push) Successful in 3m1s
ci / web (push) Successful in 1m32s
ci / bun-nix (push) Successful in 24s
ci / docs-site (push) Successful in 1m16s
android / android (push) Successful in 6m21s
decky / build-publish (push) Successful in 25s
deb / build-publish-client-arm64 (push) Successful in 57s
apple / screenshots (push) Successful in 6m0s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 3m7s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 3m27s
arch / build-publish (push) Successful in 10m32s
deb / build-publish-host (push) Successful in 6m13s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 2m9s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m2s
deb / build-publish (push) Successful in 8m38s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 40s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4m19s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 3m28s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 2m1s
docker / deploy-docs (push) Successful in 32s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5m38s
docker / builders-arm64cross (push) Successful in 3m38s
ci / rust (push) Successful in 21m50s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 9m36s
Reviewed-on: #109
2026-08-08 08:39:32 +00:00
enricobuehler e044f68500 fix(ci/arch): v0.25.0 shipped a host no Arch box can install, and nothing could tell
apple / swift (pull_request) Successful in 1m38s
apple / screenshots (pull_request) Skipped
android / android (pull_request) Successful in 5m47s
ci / rust-arm64 (pull_request) Successful in 2m35s
ci / web (pull_request) Successful in 1m53s
ci / docs-site (pull_request) Successful in 1m24s
ci / bun-nix (pull_request) Successful in 26s
ci / rust (pull_request) Successful in 7m26s
Arch moved FFmpeg 8 -> 9 (every libav soname +1) hours before the release. PR #108
fixed the real bug — packaging/arch/PKGBUILD now binds punktfunk-host to the sonames
it actually linked, so pacman refuses an upgrade instead of bricking the install — and
re-keyed ci/arch-ci.Dockerfile so the builder would carry FFmpeg 9.

The tag was pushed four minutes later. arch.yml and docker.yml have no `needs:` between
them, and arch.yml deliberately runs no -Syu ("the image's snapshot IS the build
environment"), so the release build pulled the still-FFmpeg-8 `:latest` and published

    punktfunk-host 0.25.0-1  depends: libavcodec.so=62-64, libavutil.so=60-64,
                                      libavfilter.so=11-64, libavdevice.so=62-64,
                                      libswscale.so=9-64

against a world that had moved to 63/61/12/63/10. It fails safely — pacman refuses,
nothing bricks — but it fails broadly: pacman prepares one transaction, so an
unsatisfiable dependency of OURS stopped affected users' entire `pacman -Syu`.

Nothing in the pipeline could have caught it. The existing assert proves the dep is
VERSIONED; it cannot prove the version EXISTS. So two guards, plus the lever to repair
a release that has already shipped:

* Preflight parity — compare the builder's libav `provides` against the live repos and
  `-Syu` the container if they differ. The image is a cache and may lag; on this one
  axis it may not. Syncs into a throwaway --dbpath so the container never sits in the
  partial-upgrade state a bare `pacman -Sy` leaves.

* Publish gate — resolve every built package with `pacman -U --print` against a
  PRISTINE --dbpath. Empty db means "nothing is installed", so every dependency must
  come from the repos exactly as on a user's box. Resolving against the builder's own
  installed set is what would hide this: a stale ffmpeg satisfies a stale bound.
  gamescope stays best-effort (dropped from the upload with a warning, never fatal).

* workflow_dispatch(release_tag, pkgrel) — a published release cannot be repaired by
  re-running its tag: pkgrel would stay 1, which is invisible to a box that already
  recorded the broken build, and the workflow file at the tag can never carry inputs
  added after it. Dispatched from main it takes the WORKFLOW from main and the SOURCE
  from the tag, publishes to the stable repo at a higher pkgrel, and replaces the
  release-page assets (prune_release_assets: upsert replaces by NAME, and a rebuild's
  filenames differ, so the superseded package would otherwise stay one click away).

Verified on a real ffmpeg-9 box (.21, CachyOS) rather than reasoned about: the gate
rejects the published 0.25.0-1 host with the user-visible error verbatim, and passes
client, web, scripting and gamescope — 0 false positives across all five artifacts.
The parity snippet reads today's `provides` correctly (`-Si --dbpath` on an empty db
works; pacman does not wrap fields when piped). Version logic exercised on all four
paths: rebuild -> 0.25.0-2 stable, tag push and canary unchanged, pkgrel=1 refused.

Ships as punktfunk-host 0.25.0-2. README gains the pacman error and what to do about
it; CHANGELOG says plainly that 0.25.0's Arch packages were wrong.
2026-08-08 10:34:11 +02:00
enricobuehler 8c94e2517e fix(plugin-kit): the bun.lock I committed was corrupt, and it blocked the release
audit / cargo-audit (push) Canceled after 0s
audit / bun-audit (plugin-kit) (push) Canceled after 0s
audit / bun-audit (sdk) (push) Canceled after 0s
audit / bun-audit (web) (push) Canceled after 0s
audit / docs-site-audit (push) Canceled after 0s
audit / pnpm-audit (push) Canceled after 0s
audit / license-gate (push) Canceled after 0s
ci / rust-arm64 (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bun-nix (push) Canceled after 0s
ci / rust (push) Canceled after 0s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 14s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 27s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 20s
plugin-kit-publish / publish (push) Successful in 55s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 15s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 13s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 44s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 41s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 13s
docker / deploy-docs (push) Successful in 34s
docker / builders-arm64cross (push) Successful in 3m18s
nix / flake (push) Failing after 12m55s
`plugin-kit-v0.3.2` failed at its very first real step:

    error: Duplicate package path
        at bun.lock:71:5
    InvalidPackageKey: failed to parse lockfile: 'bun.lock'
    warn: Ignoring lockfile
    error: lockfile had changes, but lockfile is frozen

`@punktfunk/host` was listed TWICE, byte-identically, at lines 69 and 71. I
introduced it: the lock had exactly one entry before 10a0ef32 and two after.
Running `bun install` to add the biome devDependency duplicated the `file:../sdk`
entry — the same `file:`-dependency lock corruption already recorded against the
web workspace's overrides.

Nothing else in the lock is wrong, so this removes the duplicate entry rather than
regenerating (a regenerate risks reproducing it, since the `file:` dep is the
cause).

Verified with the exact commands the publish workflow runs, in order:
`bun install --frozen-lockfile --ignore-scripts` (the step that failed) now
succeeds, then the file:-dep repair, `bun run check`, `bun run typecheck`,
`bun test` 67/67, `bun run build` — all clean.

No source change; 0.3.2 is unpublished, so the tag moves to this commit.
plugin-kit-v0.3.2
2026-08-08 02:51:52 +02:00
enricobuehler 08525e618e Merge pull request 'chore(release): bump workspace version to 0.25.0' (#56) from worktree-release-0250 into main
audit / cargo-audit (push) Canceled after 0s
audit / bun-audit (plugin-kit) (push) Canceled after 0s
audit / bun-audit (sdk) (push) Canceled after 0s
audit / bun-audit (web) (push) Canceled after 0s
audit / docs-site-audit (push) Canceled after 0s
audit / pnpm-audit (push) Canceled after 0s
audit / license-gate (push) Canceled after 0s
ci / rust (push) Canceled after 0s
ci / rust-arm64 (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bun-nix (push) Canceled after 0s
apple / swift (push) Successful in 1m38s
nix / flake (push) Canceled after 0s
android-screenshots / screenshots (push) Successful in 2m1s
android / android (push) Successful in 6m3s
decky / build-publish (push) Successful in 42s
apple / screenshots (push) Successful in 5m54s
arch / build-publish (push) Successful in 10m4s
release / apple (push) Successful in 11m30s
sbom / sbom (push) Successful in 30s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Canceled after 0s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Canceled after 0s
windows / build (aarch64-pc-windows-msvc) (push) Canceled after 0s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
web-screenshots / screenshots (push) Successful in 5m28s
linux-client-screenshots / screenshots (push) Successful in 2m15s
deb / build-publish-client-arm64 (push) Successful in 1m39s
deb / build-publish (push) Successful in 24m28s
deb / build-publish-host (push) Successful in 6m52s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m23s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m12s
windows-host / package (push) Successful in 14m39s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Successful in 36s
docker / deploy-docs (push) Failing after 1m11s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 4m32s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 5m52s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 2m34s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5m43s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 4m28s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8m19s
docker / builders-arm64cross (push) Successful in 3m26s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m48s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m52s
flatpak / build-publish (push) Successful in 12m16s
Reviewed-on: #56
v0.25.0
2026-08-08 00:45:46 +00:00
enricobuehler fa2bcd9dbb docs(release): the last three PRs — FFmpeg 9, the Arch soname trap, and the Linux buffer ceiling that was defeating A/V sync
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m9s
apple / swift (pull_request) Successful in 1m38s
apple / screenshots (pull_request) Skipped
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m12s
android / android (pull_request) Successful in 4m47s
ci / web (pull_request) Successful in 2m27s
ci / bun-nix (pull_request) Successful in 40s
ci / rust-arm64 (pull_request) Successful in 5m18s
ci / docs-site (pull_request) Successful in 1m26s
nix / flake (pull_request) Failing after 17m20s
ci / rust (pull_request) Successful in 23m58s
2026-08-08 02:43:46 +02:00
enricobuehler 353091270d Merge remote-tracking branch 'origin/main' into worktree-release-0250 2026-08-08 02:42:00 +02:00
enricobuehler 86bb09e2cf Merge pull request 'Arch could upgrade FFmpeg out from under the host and brick it — and the host now builds against FFmpeg 9' (#108) from worktree-ffmpeg9-support into main
apple / swift (push) Successful in 1m28s
android / android (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 0s
audit / cargo-audit (push) Canceled after 0s
audit / bun-audit (plugin-kit) (push) Canceled after 0s
audit / bun-audit (sdk) (push) Canceled after 0s
audit / bun-audit (web) (push) Canceled after 0s
audit / docs-site-audit (push) Canceled after 0s
audit / pnpm-audit (push) Canceled after 0s
audit / license-gate (push) Canceled after 0s
ci / rust (push) Canceled after 0s
ci / rust-arm64 (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bun-nix (push) Canceled after 0s
deb / build-publish (push) Canceled after 0s
deb / build-publish-host (push) Canceled after 0s
deb / build-publish-client-arm64 (push) Canceled after 0s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 0s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 0s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Canceled after 0s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
flatpak / build-publish (push) Canceled after 0s
nix / flake (push) Canceled after 0s
release / apple (push) Canceled after 2m25s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
windows-host / package (push) Canceled after 0s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Canceled after 4s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Canceled after 0s
windows / build (aarch64-pc-windows-msvc) (push) Canceled after 0s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
decky / build-publish (push) Successful in 1m6s
Reviewed-on: #108
2026-08-08 00:41:33 +00:00
enricobuehler deeb8b6700 feat(pf-encode): build against FFmpeg 9
apple / swift (pull_request) Successful in 1m53s
apple / screenshots (pull_request) Skipped
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 2m34s
ci / web (pull_request) Successful in 2m32s
ci / docs-site (pull_request) Successful in 1m25s
ci / bun-nix (pull_request) Successful in 26s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 3m23s
android / android (pull_request) Successful in 6m47s
ci / rust-arm64 (pull_request) Successful in 8m49s
nix / flake (pull_request) Failing after 16m7s
ci / rust (pull_request) Successful in 23m39s
ffmpeg-next 8.1.0 could not accept FFmpeg 9 at all: ffmpeg-sys-next's version probe
covered avcodec majors 56..62 (the range is exclusive of its end), so libavcodec 63 fell
outside what it knew how to bind. 9.0.0 widens that to 56..63, which is what actually
unblocks Arch. Bump both pins — the unconditional Linux dep and the optional Windows
amf-qsv one — and the lock with them.

No API drift to fix. The crate major is a CEILING, not a target: one source tree still
spans FFmpeg 7.x/libavcodec 61, 8.x/62 and 9.x/63 via per-version cfgs, and every wrapper
symbol the NVENC-libav, VAAPI and amf-qsv backends name survives 8.1.0 -> 9.0.0
unchanged. The three hand-written #[repr(C)] hwcontext mirrors are the parts no compiler
checks, so they were re-read against the real headers rather than trusted:
AVCUDADeviceContext and AVD3D11VAFramesContext are byte-identical across 7.1/8/9, and
AVD3D11VADeviceContext gained two trailing UINTs in 8 that 7.1 lacks — which is why that
mirror deliberately stops at the common prefix, and why its assertions now say what they
do and do not buy you. They pin our layout, not libav's; a green build is not evidence.

The CI image is the step that makes this reach users. arch.yml deliberately runs no -Syu
("the image's snapshot IS the build environment"), so the builder stayed frozen on ffmpeg
8 no matter what Arch shipped, and a canary built from that snapshot could not satisfy the
soname dep the PKGBUILD now derives. Re-keying ci/ rebuilds it against ffmpeg 9.

Ubuntu and Windows deliberately stay put: the noble .deb bundles its own FFmpeg 8 behind
an rpath and strips the libav sonames from its Depends, and Windows bundles BtbN DLLs into
the signed installer — neither is exposed to the break, BtbN publishes no FFmpeg 9 build,
and moving either would re-qualify an encode stack to buy nothing.

Verified end to end on 192.168.1.21 (CachyOS, system ffmpeg 2:9.0-5, RTX 5070 Ti): host
builds clean and links libavcodec.so.63/libavutil.so.61/libavfilter.so.12/libswscale.so.10
with no unresolved sonames; the ffmpeg-8 compat shim is gone and the service runs with
NRestarts=0 and answers 401 on :47990; pf-encode's 67 tests pass; and a live synthetic
encode drives real NVENC hardware through FFmpeg 9's libavcodec to a decodable 1080p HEVC
stream (180/180 frames, FEC loopback 0 mismatches) with libavcodec.so.63 and
libnvidia-encode both mapped into the encoding process.
2026-08-08 02:35:20 +02:00
enricobuehler b1e0525872 fix(packaging/arch): pacman could upgrade FFmpeg out from under the host and brick it
`depends=('ffmpeg' ...)` carried no version bound, and pacman is the only one of our
packaging formats that does not derive dependencies from ELF DT_NEEDED — rpm
auto-generates `libavcodec.so.62()(64bit)`, dpkg-shlibdeps emits `libavcodec62`, nix
pins the closure. So when Arch shipped ffmpeg 2:9.0-5 on 2026-08-08 and every soname
moved (libavutil .60->.61, libavcodec .62->.63, libavfilter .11->.12, libavdevice
.62->.63, libswscale .9->.10), a plain `pacman -Syu` walked every Arch/CachyOS install
straight across the break. The result is not a crash we can log: the dynamic loader
cannot start the binary at all, so it is exit 127 *before* main() in a systemd restart
loop, and because punktfunk-web is a separate bun service with no libav linkage it keeps
serving happily while :47990 has nothing listening — which reads as "the mgmt API is
broken" rather than "the host is not running". `ldd /usr/bin/punktfunk-host | grep
"not found"` is the one-line diagnosis.

Depend on the sonames instead of the package. Arch's ffmpeg declares the matching
`provides=(libavcodec.so=63-64 ...)`, and makepkg rewrites each bare `libfoo.so` listed
in depends into `libfoo.so=<soname>-<arch>` by reading the built binary's DT_NEEDED, so
the bound tracks whatever FFmpeg the builder linked against with nothing to hand-maintain
across the next bump. pacman now refuses the ffmpeg upgrade rather than bricking the
install. A hand-written `ffmpeg<2:9` would have gone stale on the very next major; not
bundling FFmpeg the way the .deb does, because that exists only because Ubuntu 24.04 LTS
is frozen on 6.1 and can never satisfy the dep, while rolling Arch always ships a current
one.

Verified on a real ffmpeg-9 box (192.168.1.21): the built package records
libavcodec.so=63-64, libavutil.so=61-64, libavfilter.so=12-64, libavdevice.so=63-64 and
libswscale.so=10-64, exactly matching DT_NEEDED, with the two libs --as-needed drops left
bare and satisfied by any ffmpeg.

The new arch.yml step asserts that expansion actually happened. If it ever stops — Arch
dropping the soname provides, someone tidying the entries out of depends — the dep
silently degrades to an unversioned name that any ffmpeg satisfies, which is exactly the
state that caused this, and it is invisible in a green build until a box bricks weeks later.
2026-08-08 02:34:59 +02:00
enricobuehler 4adea10557 Merge pull request 'fix(plugin-kit): regSubKeys could never return a subkey, and adopt biome' (#107) from worktree-kit-regsubkeys into main
audit / cargo-audit (push) Successful in 32s
audit / bun-audit (plugin-kit) (push) Failing after 23s
audit / bun-audit (sdk) (push) Successful in 20s
audit / docs-site-audit (push) Successful in 25s
audit / bun-audit (web) (push) Failing after 26s
audit / pnpm-audit (push) Successful in 12s
ci / rust-arm64 (push) Successful in 1m37s
ci / bun-nix (push) Successful in 28s
ci / web (push) Successful in 1m21s
ci / docs-site (push) Successful in 1m29s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 11s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 19s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 12s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 15s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 22s
audit / license-gate (push) Successful in 6m4s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m0s
plugin-kit-publish / publish (push) Failing after 34s
docker / builders-arm64cross (push) Successful in 9s
docker / deploy-docs (push) Successful in 35s
ci / rust (push) Canceled after 8m5s
nix / flake (push) Canceled after 3m25s
Reviewed-on: #107
2026-08-08 00:23:46 +00:00
enricobuehler 4edb662b63 fix(plugin-kit): regSubKeys could never return a subkey
ci / rust-arm64 (pull_request) Successful in 2m40s
ci / web (pull_request) Successful in 1m13s
ci / bun-nix (pull_request) Successful in 22s
ci / docs-site (pull_request) Successful in 1m20s
ci / rust (pull_request) Successful in 11m48s
nix / flake (pull_request) Successful in 16m39s
Found on hardware by the GOG plugin's own parity gate, on a box with exactly one
GOG game installed:

    HKLM\SOFTWARE\WOW6432Node\GOG.com\Games -> 1 subkey (IRON NEST ...)
    host's built-in scanner:  1 entry
    plugin:                   detect: absent, 0 games
    parity FAILED - 1 missing, exit 1

`reg.exe` ALWAYS echoes the full hive name in its output rows, never the
abbreviation it was given: query `HKLM\SOFTWARE\...` and every line comes back
`HKEY_LOCAL_MACHINE\SOFTWARE\...`. regSubKeys built its match prefix from the
`HKLM\...` string it was handed, so no line ever matched and it returned `[]` —
on every machine, for every key, always. Measured verbatim on .173:

    reg.exe:     [HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\GOG.com\Games\2013434102]
    regSubKeys:  []

Its only consumer is the GOG plugin, so the symptom was "GOG reports no games
installed" rather than an error — the same shape as the SQLite reader in 0.3.1:
a total failure that every layer degrades into an empty library.

The contract was wrong too, and the hive bug hid it. regSubKeys returned whole
key PATHS while the GOG plugin uses each result as a bare NAME
(`const key = \`${GAMES_KEY}\\${id}\``, and the subkey name IS the product id
that becomes `external_id`). Even with the prefix fixed, paths would have
composed nonsense keys. It now returns names, which is what the sole consumer
and its own comment always assumed.

Parsing is split into an exported `parseRegSubKeys(stdout, key)` for the same
reason `parseRegQuery` is exported — this is a text format that breaks quietly,
and it had NO test coverage at all. Six added, using the verbatim .173 output:
names not paths, multiple subkeys, grandchildren ignored, the queried key is not
its own subkey, case-insensitivity, and empty/error input. Four of the six FAIL
against the old behaviour.

0.3.1 -> 0.3.2. Gates: biome clean, tsc clean, 67/67 tests, build clean.
2026-08-08 02:19:30 +02:00
enricobuehler 10a0ef3283 style(plugin-kit): adopt the biome config its own plugins already use
The kit had NO biome config and no lint script, while every plugin repo that
consumes it has both. So its source quietly drifted — unused imports, unsorted
imports, formatting — with nothing to catch any of it. Running biome here for
the first time reported 20 findings across 8 files.

Adds `plugin-kit/biome.json` mirroring the plugin repos' (tab indent, double
quotes, recommended lint preset, organizeImports), a `check` script, and
`@biomejs/biome` pinned to the same `^2.5.2` the plugins pin — without that pin
`bunx biome` resolved 2.4.6, which rejects the 2.5 `rules.preset` key.

Two deliberate differences from the plugin repos' copy:

  * no `vcs.useIgnoreFile` — those are standalone repos with a .gitignore beside
    the config; plugin-kit is a directory inside this one, and biome errors with
    "couldn't find an ignore file". The `files.includes` exclusions cover it.
  * `!examples/**/dist` instead of `!ui/dist` — the kit has examples, not a UI.

`css.parser.tailwindDirectives` is carried over and is load-bearing: without it
biome cannot parse `@theme` in src/theme.css and reports three parse errors on
CSS that is perfectly valid Tailwind v4.

Everything here is formatter/import churn except two real findings, both fixed:

  * `Layer` (library/define.ts) and `Cause` (sync-engine.ts) were imported and
    never used;
  * test/spike-httpapi.test.ts read `(reg?.body as …).ui.secret` one line after
    `expect(reg).toBeDefined()`. The optional chain undoes the assertion: had
    `reg` been undefined the `.ui` access would throw a TypeError instead of
    failing the test readably. Now asserted to the type system too.

Wired into plugin-kit-publish.yml as a `Lint & format` step ahead of Typecheck,
so this cannot rot again.

Gates after: biome clean (42 files), tsc clean, 67/67 tests, build clean.
2026-08-08 02:19:06 +02:00
enricobuehler cabd011f1d Merge pull request 'The 272 ms audio buffer was legal: the PipeWire callback filled the buffer ceiling, not the graph's request' (#106) from fix/pw-playback-requested into main
apple / swift (push) Successful in 1m34s
ci / web (push) Successful in 1m3s
ci / bun-nix (push) Successful in 17s
ci / docs-site (push) Successful in 1m15s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m37s
ci / rust-arm64 (push) Successful in 3m1s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 8s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 9s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 13s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 27s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 23s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 17s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 17s
android / android (push) Successful in 5m6s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m43s
apple / screenshots (push) Successful in 6m5s
deb / build-publish (push) Successful in 4m8s
deb / build-publish-host (push) Successful in 3m54s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m9s
deb / build-publish-client-arm64 (push) Successful in 4m36s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m11s
ci / rust (push) Canceled after 2m31s
docker / builders-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
arch / build-publish (push) Successful in 8m34s
flatpak / build-publish (push) Successful in 18m15s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m28s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 22m4s
Reviewed-on: #106
2026-08-08 00:12:02 +00:00
enricobuehler d939c7c14e Merge pull request 'fix(pf-inject): the DualShock 4 Windows backend never imported OFF_INPUT' (#105) from worktree-ds4-off-input into main
ci / web (push) Successful in 1m12s
ci / docs-site (push) Successful in 1m18s
android / android (push) Canceled after 1m46s
apple / swift (push) Canceled after 1m53s
apple / screenshots (push) Canceled after 0s
ci / bun-nix (push) Successful in 24s
ci / rust-arm64 (push) Successful in 1m47s
arch / build-publish (push) Canceled after 2m6s
ci / rust (push) Canceled after 37s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 14s
deb / build-publish (push) Canceled after 0s
deb / build-publish-client-arm64 (push) Canceled after 29s
deb / build-publish-host (push) Canceled after 30s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 10s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Canceled after 4s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
windows-host / package (push) Successful in 15m9s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 27s
Reviewed-on: #105
2026-08-08 00:10:18 +00:00
enricobuehler be86cfcdc0 fix(client/audio): the PipeWire callback stops filling the buffer ceiling every cycle
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m12s
apple / swift (pull_request) Successful in 1m38s
apple / screenshots (pull_request) Skipped
ci / bun-nix (pull_request) Successful in 23s
ci / web (pull_request) Successful in 1m0s
ci / docs-site (pull_request) Successful in 1m8s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m18s
ci / rust-arm64 (pull_request) Successful in 1m40s
android / android (pull_request) Successful in 5m24s
ci / rust (pull_request) Successful in 12m51s
The playback process callback sized its writes from the mapped buffer's
capacity — PipeWire's quantum-limit, 8192 frames ≈ 170 ms — instead of
the graph's per-cycle ask (pw_buffer.requested). Every cycle therefore
queued up to 170 ms of PCM downstream of the ring, and, worse, taught
JitterPolicy that the device drains 170 ms per callback: the underrun
floor (want + one frame) rose above any depth the A/V sync loop may
request, so sync measured audio ~280 ms late and was forbidden — by its
own continuity rule — from draining it. The first on-glass run of the
latency overhaul showed exactly that: audio buffer 272 ms, a/v +284 ms,
stable.

Honor requested (capacity remains both the ceiling and the fallback for
requested == 0), and log requested-vs-capacity once per stream in the
shape of the host's per-capture-open quantum line, so the next on-glass
report can say which one is sizing the writes.

Needs libpipewire >= 0.3.49 (2022-03) for the requested field; every
ship target clears that.

Verified on .21: cargo clippy -p pf-client-core --all-targets -D
warnings clean, 167 tests pass, fmt clean.
2026-08-08 02:04:15 +02:00
enricobuehler e9e1ec7dc5 fix(pf-inject): the DualShock 4 Windows backend never imported OFF_INPUT
ci / web (pull_request) Successful in 1m22s
apple / swift (pull_request) Successful in 1m32s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 2m58s
ci / docs-site (pull_request) Successful in 1m25s
ci / bun-nix (pull_request) Successful in 30s
android / android (pull_request) Successful in 5m2s
ci / rust (pull_request) Successful in 8m2s
The Windows host does not build:

  error[E0425]: cannot find value `OFF_INPUT` in this scope
    --> crates\pf-inject\src\inject\windows\dualshock4_windows.rs:65:48
  error: could not compile `pf-inject` (lib) due to 1 previous error

`dualshock4_windows.rs` writes the neutral report straight to `OFF_INPUT` in its
bootstrap path — correctly, and exactly as the DualSense and Steam Deck backends
do: the devnode does not exist yet at that point, so there is no reader to race
and no seqlock to take. Its steady-state path already goes through
`publish_input`, which is the v2.3 seqlock.

But the import list only names `publish_input`. `steam_deck_windows.rs` imports
`OFF_INPUT` explicitly for the same bootstrap write; this one was missed when the
list was edited to add `publish_input`.

One word in a `use`. No behaviour.

WHY CI DID NOT CATCH IT: `pf-inject`'s Windows backends compile only for
`*-pc-windows-msvc`, and the crate is host-side, so the client Windows workflow
never touches it. A cargo check from a Mac cannot stand in either — pf-inject
pulls punktfunk-core and therefore ring, whose C build wants MSVC headers, so the
cross-check dies in cc-rs long before it reaches this file.

FOUND BY: running windows-host.yml's own build line on the CI runner (.133)
against the v0.25.0 release tree before tagging —
`cargo build --release -p punktfunk-host --features nvenc,amf-qsv,qsv`. It fails
at `pf-inject`, which is step 1 of the host job, so a v0.25.0 tag would have
produced no Windows host binary, no installer, and no host asset on the release.
2026-08-08 01:43:11 +02:00
enricobuehler cacfe04a93 docs(release): the commit count catches up with main (327 -> 395)
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m26s
apple / swift (pull_request) Successful in 1m36s
apple / screenshots (pull_request) Skipped
android / android (pull_request) Successful in 4m30s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m33s
ci / web (pull_request) Successful in 1m24s
ci / bun-nix (pull_request) Successful in 22s
ci / rust-arm64 (pull_request) Successful in 2m34s
ci / docs-site (pull_request) Successful in 1m27s
ci / rust (pull_request) Successful in 14m47s
nix / flake (pull_request) Failing after 13m47s
2026-08-08 01:41:09 +02:00
enricobuehler 5fda4d4805 Merge remote-tracking branch 'origin/main' into worktree-release-0250
android / android (pull_request) Canceled after 9s
apple / swift (pull_request) Canceled after 0s
apple / screenshots (pull_request) Canceled after 0s
ci / rust (pull_request) Canceled after 3s
ci / rust-arm64 (pull_request) Canceled after 2s
ci / web (pull_request) Canceled after 0s
ci / docs-site (pull_request) Canceled after 0s
ci / bun-nix (pull_request) Canceled after 0s
nix / flake (pull_request) Canceled after 0s
windows / build (aarch64-pc-windows-msvc) (pull_request) Canceled after 0s
windows / build (x86_64-pc-windows-msvc) (pull_request) Canceled after 0s
2026-08-08 01:40:04 +02:00
enricobuehler 959d3eb604 docs(release): A/V sync takes a TL;DR slot, and the old audio claim was wrong
android / android (pull_request) Canceled after 0s
apple / swift (pull_request) Canceled after 0s
apple / screenshots (pull_request) Canceled after 0s
ci / rust (pull_request) Canceled after 0s
ci / rust-arm64 (pull_request) Canceled after 0s
ci / web (pull_request) Canceled after 0s
ci / docs-site (pull_request) Canceled after 0s
ci / bun-nix (pull_request) Canceled after 0s
nix / flake (pull_request) Canceled after 0s
windows / build (aarch64-pc-windows-msvc) (pull_request) Canceled after 0s
windows / build (x86_64-pc-windows-msvc) (pull_request) Canceled after 0s
main moved another 62 commits (a8a4b11f -> fca9f42c), taking 0.25.0 to 391 since
v0.24.0. Five PRs: decode aliasing (#102), A/V sync (#101), gyro correctness
(#99), web console sweep (#100), Apple ATS (#103).

THE CORRECTION THAT MATTERED. The notes carried "Audio that falls behind the
picture pulls itself back … Android was worst, with no correction at all",
describing the jitter ring's buffer-shedding as if it were sync. It never was.
The host has stamped `pts_ns` on every audio datagram since long before v0.24.0
and EVERY CLIENT DECODED IT AND NEVER READ IT — verified in the v0.24.0 tree
(`crates/punktfunk-host/src/native/audio.rs:162` stamps it; the client audio
paths ignore it). Lip-sync was an emergent property of buffer depth, and it got
WORSE as video got faster, which is why shaving milliseconds off the audio budget
had never helped. That bullet is rewritten to say what is actually true, and A/V
sync takes a TL;DR slot.

It displaces the settings-BOM bullet, which was the weakest of the six as a
HEADLINE: conditional (only if the file was ever saved by PowerShell), partly
duplicated by the Windows non-C: entry, and it survives verbatim in Fixed. A/V
sync affects every user, every session, every client, with sound on — and unlike
most of this release it shipped broken in EVERY release we have ever made.

GYRO NEEDS AN UPGRADE NOTE, so it got one. The pipeline was wrong end to end and
is now measured against a real controller, which MOVES AIM SENSITIVITY: a pad
presented as a DualShock 4 reported gyro 40x fast (host-side), and a PlayStation
pad on Android reported ~30% short (client-side). At 40x nobody could have
compensated — gyro aim was unusable, not miscalibrated — but the Android ~1.4x
change is exactly the size a real person tunes around, so `## Before you update`
names it specifically.

DELIBERATELY NOT PROMOTED. The decode-aliasing program (#102) reads like a
catastrophe — H.264 decoding into a surface it predicted from on 297 of every 300
access units of every stream we emit, on both rungs — but it NEVER SHIPPED:
`git ls-tree v0.24.0 crates/` has no pf-vkdecode/pf-dxvadec/pf-vaadec/pf-bitstream.
It is a ship-blocker that was cleared, and writing "your picture was subtly wrong"
would be false for every reader. It contributes one clause to the decode entry
(every path is now checked frame-by-frame against a reference decoder; Windows +
Intel AV1 routes through Direct3D) and a full section in the changelog. Same
reasoning already applied to #96 and the rav1d abort.

Changelog gains the A/V sync mechanism (including that video is the master and
continuity outranks sync — the ring refuses a sync request that would break audio
on a jittery link) and the aliasing section, with the four independent reasons
four gates missed it: a structurally-blind conformance vector, a test that had
encoded the bug AS CORRECT, a vacuous assertion that could not fail, and the fact
that it streamed clean on glass. gpu_parity is 11 legs, not the 9 an earlier note
claimed.

Verified after the merge: lock diff versions-only 35/35, `cargo metadata --locked`
resolves (39 members), `cargo fmt --all --check` clean in both workspaces, notes
body 0 internal-vocabulary hits, Play notes 497/500 by android.yml's own gate.
Wire 2, C ABI 17, no new capability bits in this range.
2026-08-08 01:36:53 +02:00
enricobuehler b2f08b1a73 Merge pull request 'fix(pf-dxvadec): a wrapped sentence turned "6." into an ordered list' (#104) from worktree-dxva-doclint into main
ci / web (push) Successful in 1m35s
ci / docs-site (push) Successful in 1m35s
ci / bun-nix (push) Successful in 1m16s
ci / rust-arm64 (push) Successful in 2m8s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 14s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 18s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 1m1s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m2s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 1m11s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 19s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 27s
android / android (push) Successful in 5m46s
deb / build-publish-client-arm64 (push) Successful in 3m56s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m52s
apple / swift (push) Successful in 1m35s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m32s
deb / build-publish-host (push) Successful in 5m26s
arch / build-publish (push) Successful in 8m58s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 8s
docker / builders-arm64cross (push) Successful in 9s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m57s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m39s
apple / screenshots (push) Successful in 6m16s
docker / deploy-docs (push) Failing after 4m26s
ci / rust (push) Successful in 7m45s
deb / build-publish (push) Successful in 7m51s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 22m35s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 22m48s
Reviewed-on: #104
2026-08-07 23:26:52 +00:00
enricobuehler 7f82bca9c0 fix(pf-dxvadec): a wrapped sentence turned "6." into an ordered list
ci / bun-nix (pull_request) Successful in 23s
ci / web (pull_request) Successful in 1m10s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m17s
ci / docs-site (pull_request) Successful in 2m3s
ci / rust-arm64 (pull_request) Successful in 2m16s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m10s
apple / swift (pull_request) Successful in 1m34s
apple / screenshots (pull_request) Skipped
android / android (pull_request) Successful in 4m59s
ci / rust (pull_request) Successful in 6m3s
A doc paragraph in `pic_av1.rs` wrapped so that "first at frame / 6. Releasing…"
put `6.` at the start of a line. rustdoc reads that as an ordered-list item
starting at 6, which makes the following unindented `///` line a lazy
continuation — `clippy::doc_lazy_continuation`, denied by `-D warnings`.

Reflowed so the number cannot begin a line. Prose is byte-identical in content;
only the wrap points move. No code, no behaviour.

WHY THIS MATTERS FOR THE TAG. `pf-dxvadec` is Windows-only, and no Windows leg
runs on a push to main — so main being green proves nothing about this. The
failure surfaces for the first time in a release tag's fan-out, which is exactly
what happened to the FIRST v0.23.0 tag: it went red on Windows clippy for this
same lint, and the cure was a tag re-point.

Caught pre-tag by re-running the lazy-continuation scanner over the tree while
preparing v0.25.0 (0 hits before this commit's parent merged the new decode
crates, 1 after). Cannot be verified by compiling here — the crate does not build
on macOS — so the evidence is the scanner plus the lint's own rule, not a clippy
run.
2026-08-08 01:25:59 +02:00
enricobuehler 9043332002 Merge remote-tracking branch 'origin/main' into worktree-release-0250 2026-08-08 01:24:23 +02:00
enricobuehler fca9f42c44 Merge pull request 'Worktree apple mgmt ats bypass' (#103) from worktree-apple-mgmt-ats-bypass into main
apple / swift (push) Successful in 1m39s
ci / rust-arm64 (push) Successful in 2m3s
ci / web (push) Successful in 1m4s
ci / bun-nix (push) Successful in 31s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 14s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 12s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 13s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 15s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 15s
ci / docs-site (push) Successful in 1m58s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 39s
apple / screenshots (push) Canceled after 0s
ci / rust (push) Canceled after 49s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 3s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
release / apple (push) Successful in 9m42s
Reviewed-on: #103
2026-08-07 23:20:19 +00:00
enricobuehler 3d20f2c0e5 Merge pull request 'Three decode rungs were decoding into a surface they were predicting from' (#102) from integration/decode-aliasing-program into main
ci / bun-nix (push) Successful in 30s
ci / web (push) Successful in 1m22s
ci / docs-site (push) Successful in 1m24s
apple / swift (push) Successful in 1m35s
ci / rust-arm64 (push) Successful in 1m56s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 16s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 16s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 16s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 14s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
deb / build-publish-client-arm64 (push) Successful in 3m6s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m11s
android / android (push) Successful in 5m57s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m21s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m37s
apple / screenshots (push) Successful in 5m52s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m24s
deb / build-publish-host (push) Successful in 6m20s
ci / rust (push) Canceled after 6m56s
docker / builders-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
windows-host / package (push) Failing after 1m37s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
deb / build-publish (push) Successful in 5m39s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m47s
arch / build-publish (push) Successful in 11m19s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m19s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 6m37s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 10m20s
flatpak / build-publish (push) Successful in 8m54s
Reviewed-on: #102
2026-08-07 23:11:28 +00:00
enricobuehler 7798401f06 perf(apple): cache posters on disk and pool the mgmt connections
ci / rust-arm64 (pull_request) Successful in 1m36s
ci / bun-nix (pull_request) Successful in 24s
ci / web (pull_request) Successful in 1m16s
apple / swift (pull_request) Successful in 1m38s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m12s
ci / rust (pull_request) Successful in 13m3s
Moving the management API onto Network.framework left one request per
connection, so a library grid paid a TLS handshake per poster where the pooled
URLSession had shared one. And the Apple client -- unlike Windows -- never
cached art at all, so it re-fetched every poster on every visit.

ArtCache: a size- and age-bounded blob cache in the CACHES directory (every byte
is re-derivable from the host, so the system is welcome to evict it). Keyed by
the SHA-256 of the absolute URL, so host-proxy paths and store CDN URLs share
one cache without colliding. Reads touch the entry, so eviction is by last USE,
not last write. Empty bodies and data: URLs are refused -- neither is worth a
file. Defaults: 128 MB, 30 days.

Connection pooling: MgmtConnectionPool keeps up to four keep-alive connections
per host and makes further callers wait rather than opening more, which is the
part that matters -- a grid can ask for dozens of posters at once. A connection
the host dropped since we last used it is indistinguishable from a live one
until we write, so a REUSED connection that fails is retried once on a fresh
one; a fresh failure is a real failure.

Keep-alive means a response can no longer be delimited by the peer hanging up,
so HTTPResponseParser.messageLength finds the end from the framing itself --
Content-Length or the chunked terminal chunk plus trailers. Getting that wrong
would truncate a response or bleed one into the next, silently, so it carries
the bulk of the new tests. A connection with bytes left over after a response is
dropped rather than reused: we never pipeline, so anything trailing means we are
out of sync.

LibraryView closes the loader's pooled connections on disappear instead of
leaving sockets open on a screen the user has left.

16 new tests: message framing (both encodings, partial reads, back-to-back
responses, close detection) and the cache (binary round trip, key separation,
refusals, expiry, LRU eviction).
2026-08-08 01:09:25 +02:00
enricobuehler 244cafe005 refactor(apple): move the mgmt API off URLSession so ATS can stay on
The previous commit bought the library back on VPN/remote hosts by declaring
NSAllowsArbitraryLoads, which works but is blunt: it drops ATS for ALL of the
app's URLSession traffic, and the only other traffic is third-party cover-art
CDN fetches -- the one surface we never wanted to open. It cost the TLS-version
floor, forward secrecy, and the cleartext-HTTP block on URLs the host supplies
at runtime (custom entries and scanner plugins carry arbitrary ones).

So take the host out of the URL loading system instead. MgmtTransport speaks
HTTPS over Network.framework, which ATS does not govern, and states the trust
rule we actually mean in a verify block: the leaf must hash to the fingerprint
pinned during PIN pairing. That is the same rule punktfunk-core has always
applied on the QUIC stream plane -- which is exactly why streaming kept working
over Tailscale while the library did not.

With that, the ATS dict is gone and ATS is fully enforced again. Cover-art CDN
fetches keep ordinary URLSession with full system trust evaluation and no client
certificate. LibraryTLSDelegate is deleted; nothing pins through URLSession now.

Also here:
- HTTPResponse: just enough HTTP/1.1 to read one GET -- status, headers,
  Content-Length and chunked framing (hyper streams the art proxy chunked). A
  body shorter than Content-Length throws instead of returning partial JSON,
  which would otherwise read as "this host has no games".
- LibraryError.pinMismatch, so a re-keyed host says "pair again" rather than
  sending someone to debug their network.
- 403 joins 401 as "unauthorized": both are the host declining the certificate.
- baseURL brackets IPv6 literals; the old string interpolation did not.
- 11 tests covering the framings hyper emits and the failure modes that would
  otherwise be silent.

Known trade-off: no connection reuse yet, so each poster costs its own
handshake where the pooled URLSession shared one. Fine on a LAN, worth revisiting
for large libraries over a high-latency link.
2026-08-08 01:09:04 +02:00
enricobuehler bae8742e48 fix(apple): the library's unreachable message pointed at the wrong layer
It led with `--mgmt-bind 127.0.0.1`, a rare cause, and never mentioned the one
fact that actually explains the symptom: the library rides the management API on
a different port (47990) than the QUIC stream plane (9777), so it can fail while
streaming to the same host works. Field triage of exactly that case spent hours
on the stream path before anyone questioned the port.

Leads with that now, and names browser-testing the port as the fast split between
"unreachable" and anything client-side.
2026-08-08 01:07:26 +02:00