Commit Graph
98 Commits
Author SHA1 Message Date
enricobuehlerandClaude Fable 5 35c61fee64 chore(client): the windows-rs pin moves a month forward, onto the SDK-metadata bindings
The July 2026 windows-rs brings a reconciler keyed-child-order fix (#4728), widget
validation (#4727), a DPI collision fix (#4751), icon elements (#4736), multi-window
support (#4730) and scroll virtualization (#4710) — the re-render fixes the Windows
client has been working around at the architecture level. All three pinned deps
(windows-reactor, windows, windows-reactor-setup) move together so windows-core
stays unified across the swap-chain hand-off, and pf-client-core moves with them.

The bulk of the diff is #4689: windows/windows-sys now generate straight from the
Windows SDK, so the `Win32_*` namespace features became one feature per SDK header
(winuser, dxgi, d3d11, …), the PascalCase namespace modules became header-named
modules, struct-returning COM methods take explicit out-params and return HRESULT,
Win32 functions return their raw BOOL/HANDLE instead of Result, and flag constants
are plain integers. Both crates' Win32 code is rewritten to that shape; behaviour
is unchanged on every path.

Riding along, all already stale before the bump: the README and the three Windows
workflows stop claiming windows-reactor's build.rs needs CARGO_WORKSPACE_DIR (that
build.rs no longer exists — staging moved to windows-reactor-setup via OUT_DIR);
the README layout section stops describing modules that moved into the session
binary long ago and gains the manual smoke checklist; the notices generator learns
the SPDX for crates that ship license files without a `license` field, which turns
windows-reactor-setup's UNKNOWN into MIT OR Apache-2.0; and the crate records its
real rust-version (1.96) instead of inheriting the workspace's 1.82.

Verified: cargo check/clippy/fmt clean on punktfunk-client-windows, pf-client-core
and punktfunk-client-session; both bins build; --discover finds the LAN hosts; the
GUI shell comes up (WinAppSDK bootstrap intact under the new reactor-setup).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:31:47 +02:00
enricobuehlerandClaude Fable 5 944c03dd32 feat(client): the desktop clients wear the host's OS mark
The client half of the host's new `os=` advert, shared once in pf-client-core:
`sanitize_os` (mDNS is unauthenticated input — lowercase `[a-z0-9._-]` tokens,
capped) and `os_icon_tokens`, the most-specific-first walk with the brand
aliases (`macos`→apple, `steamos`→steam) every platform resolves through.
`DiscoveredHost` carries the chain, `KnownHost` persists it (`serde(default)`,
elided when empty — older stores load unchanged and older clients read back
exactly what they wrote), `upsert` moves it only when carried, and `learn_os`
mirrors `learn_mac` — no-op, no disk write when unchanged — so the mark
survives the host going to sleep.

GTK shells: the card's status row leads with a recolorable symbolic glyph.
That needed real embedded assets — the shells had none — so `data/` gains the
ten `pf-os-*-symbolic` SVGs compiled into a gresource (new build.rs,
glib-build-tools) and registered on the icon theme at startup; the Adwaita
theme then tints them like every other status glyph.

WinUI shell: reactor renders raster-from-URI only, so the embedded mid-gray
PNGs (legible on both themes) materialize once into
%LOCALAPPDATA%\punktfunk\os-icons\ — the library's poster-art pattern — and
the tile's status row leads with a 16px image.

The couch UI plumbs `HostRow.os` (live advert preferred, else the store) for a
Skia glyph that is a declared follow-up; `--list-hosts` / `hosts --json` emit
the stored chain so the Decky plugin can read it. A host that advertises no
`os` renders everywhere exactly as it did before the field existed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 17:58:33 +02:00
enricobuehlerandClaude Opus 5 b0ea1e6b51 fix(client/linux): the override marker appears on touch, profiles get a colour, and 4:4:4 gets a switch
Three things found by actually driving the client.

**The marker didn't appear until you reopened the dialog.** It was rendered once, at build
time, from the stored overlay — so changing a setting inside a profile looked like it did
nothing. The design says touching a control creates the override and the marker appears
immediately, and it has to: a user who changes a row and sees no acknowledgement has no
reason to believe it took. Every profileable row now builds its dot and reset hidden, and
the same handler that records the touch reveals them. Resetting a row touched in the same
sitting also un-touches it, so the commit can't re-write what the reset just removed.

**Profiles had no colour.** `accent` has been in the schema since P0 and nothing could set
it, which left every chip the same grey — and telling profiles apart at a glance across a
grid is the entire reason chips exist. Creating a profile now picks a colour in the same
breath as its name (hunting for it afterwards is what leaves them all grey), an existing
profile has a Colour row, and host-card chips are tinted with it. A palette of eight rather
than a free picker: legibility across light and dark is the job, and the schema still
accepts any `#RRGGBB` a hand-edit or a future picker writes. Anything that isn't `#RRGGBB`
is refused rather than interpolated into CSS, and each distinct colour registers one
display-wide rule (per-widget providers are gone since GTK 4.10).

**4:4:4 had no switch anywhere but Apple.** `VIDEO_CAP_444` has been on the wire for a
while with only `punktfunk-probe`'s env var to set it. It is now a setting — and a
profileable one, which is the point: full chroma is what makes small text and thin UI lines
crisp, so a "Work" profile wants it where "Game" usually doesn't. The host still gates it
on its own policy, HEVC, and a GPU that can actually encode it; advertising only says "I
can decode this and I want it".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 12:38:53 +02:00
enricobuehler aa070f2d55 feat(ffi): hand-mirrored C structs are now layout-checked at compile time
The sharpest memory-safety risk left in this codebase is not an `unsafe` block — it is a
hand-written `#[repr(C)]` mirror of an external C struct. Get a field offset wrong and nothing fails
to compile and nothing reliably crashes: the library reads a pointer, a length or a pitch out of the
wrong bytes. Eleven such structs across five files had NO check at all.

Guarded here, each next to the struct it protects:

* `AVCUDADeviceContext`, and `AVD3D11VADeviceContext`/`AVD3D11VAFramesContext`. `ffmpeg-sys-next`
  binds none of them, so these mirrors are the only definitions — and we WRITE through them
  (`cuda_ctx`, `device`, `bind_flags`). ⚠ The D3D11VA pair is duplicated VERBATIM in two crates
  (pf-encode's `ffmpeg_win.rs`, pf-client-core's `video_d3d11.rs`) because neither can depend on the
  other; they must agree with libav and with each other, and now a drift in either is a build error.
* The six cuda.h structs. Three were already asserted — but only in `#[cfg(test)]`, so the check ran
  when someone ran the tests and never in a release build. They are `const` now. The other three,
  including `CUDA_MEMCPY2D` which is filled on EVERY zero-copy frame, had nothing.
* `MsghdrX`, Darwin's `msghdr_x`, which `libc` does not expose. Its layout is not reviewable by eye:
  the 32-bit fields force padding before each following pointer, so `msg_iov` sits at 16 and not 12.
  `sendmsg_x`/`recvmsg_x` take the pointer and length from it.
* `IPolicyConfigVtbl` — the sharpest of the set. It mirrors an UNDOCUMENTED COM interface, and
  `set_default_endpoint` is called by SLOT INDEX through a ten-entry `_reserved` gap that carries no
  names to anchor a review. A field added or resized above it does not break the build; it calls a
  different function pointer through a mismatched signature.

Every assertion is `const _: () = assert!(..)`, so it holds on every build including release and
cannot be skipped. The compiler verified the numbers — the sizes and offsets asserted here are the
ones the target actually produces, on each platform that compiles the struct.

Verified: Linux .21 fmt + both CI clippy steps rc=0 (CUDA + libav CUDA mirrors); Windows .47 full CI
clippy set rc=0 + pf-capture tests (D3D11VA pair, COM vtable); macOS `cargo check -p punktfunk-core`
(MsghdrX — the only platform that compiles it).
2026-07-29 08:48:42 +02:00
enricobuehler e1ddd49e37 fix(client-core,ffvk): close the proof-lint hole in two of the three unguarded crates
`clippy::undocumented_unsafe_blocks` is what makes the SAFETY convention a rule rather than a habit,
and three crates had never adopted it — pf-client-core (91 unsafe items), pf-presenter (123) and
punktfunk-core (167) — while every other subsystem crate denied it. That gap is why the decoders'
`unsafe impl Send`s carried a one-line aside instead of an argument: nothing required one.

pf-client-core and pf-ffvk now deny it, with a proof written for all 58 + 3 sites they had.

⚠️ 44 of those 58 were WINDOWS-ONLY — `clipboard.rs` 24 and `video_d3d11.rs` 20 — and invisible to
the Linux measurement that sized this work at 14. Same trap as the E0133 sweep: a Linux-only survey
of a cross-platform crate undercounts by whatever the `cfg` hides, here by 3x. Landing the deny on
the strength of that number alone would have re-broken Windows CI, which is exactly the mistake this
session already made once with the `warn`-that-was-really-`deny`.

The proofs say what is actually load-bearing rather than restating the call. In `clipboard.rs` that
is the ownership split Win32 requires and nothing in the code stated: `GetClipboardData` returns a
handle BORROWED from the clipboard (never freed here), while `GlobalAlloc` + `SetClipboardData`
TRANSFERS ownership to it (which is why nothing frees that one either) — two opposite rules, three
lines apart. In `video_d3d11.rs` the recurring one is that libav's `get_format` list is
NUL-terminated by `AV_PIX_FMT_NONE`, which is what keeps the walk in bounds.

Remaining: punktfunk-core (~146, of which `abi.rs` is 141) and pf-presenter (~108). Both want the
"state the contract once" treatment — abi.rs's sites are a handful of repeating shapes (`opt_cstr`
on caller C strings, null-guarded out-param writes, forwarding calls), not 141 distinct arguments.
Note the vendored `fec-rs` (18 sites) is a separate path-dependency crate, so it is out of scope
rather than something to prove.

Verified: Linux .21 fmt + both CI clippy steps rc=0; Windows .47 `-p pf-client-core` clippy
`-D warnings` rc=0 (the only place the 44 are visible), plus the full Windows CI clippy set and
pf-capture's 18 tests.
2026-07-29 08:48:42 +02:00
enricobuehlerandClaude Opus 5 f32c3aaa71 feat(session): spec mode — the renderer stops resolving policy and stops writing settings
C2 of design/client-architecture-split.md, and the last of the session's overreach.

`--resolved-spec <path>` hands the session everything it needs already resolved —
effective settings, the host's clipboard decision, the profile's name — and in that mode it
performs ZERO store reads. It had been re-deriving all three, which meant policy was being
evaluated inside the thing that draws pixels, and that the spawner and the child could
disagree about a file either of them might have written in between. First-party spawns
(the shells, the CLI) always pass one now.

The compat path stays for hand-run `punktfunk-session --connect` and old Decky scripts —
but it calls the SAME helper, so the two modes cannot drift; it is the identical function
invoked in-process instead of by the parent. A spec that is named but unreadable fails
loudly rather than quietly falling back: a spawner that asked for exact settings must not
get store-derived ones instead.

The match-window write-back is gone too. The callback used to load-modify-save the shared
settings file from inside the renderer — one of that file's five concurrent writers, for a
value only the parent needs. It now reports `{"window":{w,h}}` on stdout and the spawner
persists it, on a real change only. A hand-run session still persists its own window,
because nobody is listening to its stdout there and the event alone would drop the value.

Verified on .21: a spec naming a profile that doesn't exist in the catalog is honoured
(proving no lookup happened), a missing spec errors instead of falling back, and the CLI's
spec file is written and cleaned up per launch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 01:16:17 +02:00
enricobuehler d2b6f5b65f docs(unsafe): audit all 49 unsafe impl — one proof was wrong, four were missing
windows / build (aarch64-pc-windows-msvc) (push) Canceled after 0s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Canceled after 0s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Canceled after 0s
windows-host / package (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
windows-drivers / probe-and-proto (push) Canceled after 0s
windows-drivers / driver-build (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
flatpak / build-publish (push) Canceled after 0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Canceled after 0s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
decky / build-publish (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
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 / bench (push) Canceled after 0s
arch / build-publish (push) Canceled after 0s
apple / swift (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
android / android (push) Canceled after 0s
`unsafe impl Send`/`Sync` is the highest-risk unsafe category here and the one this program had
never looked at: a wrong one is cross-thread UB that is invisible at every call site, with no
`unsafe` block to catch a reviewer's eye. 49 of them (41 Send, 8 Sync). Two results.

**`MappedView`'s `Sync` proof was factually wrong.** It read "only exposes accessors that are safe
under concurrent use" — they are not. `read_u8`/`write_u8`/`read_u16` are plain unaligned accesses
through `&self`, and `&MappedView` really is shared across threads: `ChannelState::data()` hands out
`&'static MappedView`, and pf-xusb, pf-mouse and pf-gamepad all dispatch
`WdfIoQueueDispatchParallel` with `NumberOfPresentedRequests = u32::MAX`. The struct's own doc had
the right story — consistency is the channel protocol's job — but the `unsafe impl` stated a
different, stronger claim, which is the one a reviewer checking that line would rely on.

The impl is still sound, for a reason worth writing down: these bytes are mapped into ANOTHER
PROCESS that writes them concurrently, so Rust-level exclusivity over them is unachievable no matter
what this type does. Sync fields go through the atomic accessors; the plain ones cover only
protocol-fenced bytes. The proof now says that, and states the rule it implies for accessors added
later — plain path only for bytes the protocol already fences.

**Four `Send` impls carried a one-line aside instead of a proof** — the pf-client-core decoders and
`DrmFrameGuard`. All four are sound, and each now says why, including the two facts that make them
work and were nowhere stated: libav permits a codec context to be used from a thread other than its
creator provided use is serialised (`&mut self` is that serialisation), and D3D11's immediate
context is thread-AGNOSTIC rather than thread-safe — it wants serialised use, not one fixed thread.
Each also records that it is deliberately not `Sync`, which is the invariant a future `impl Sync`
would silently break.

Every one of the 49 now carries reasoning. Verified: Linux .21 fmt + both CI clippy steps rc=0;
Windows .47 `-p pf-client-core` clippy `-D warnings` rc=0 (it compiles the `video_d3d11` proof the
Linux run cannot see). The pf-umdf-util edit is comment-only — confirmed by diff, since that crate
needs the WDK, which .47 does not have.
2026-07-29 00:37:14 +02:00
enricobuehler ee29b3c3a9 refactor(client-core): three decoder lift methods that took nothing to get wrong
`D3d11Decoder::lift`, `VaapiDecoder::map_dmabuf` and `VulkanDecoder::extract` each take `&mut self`
and NOTHING else. They dereference `self.frame`, the `*mut AVFrame` the decoder allocates in its own
constructor and owns until `Drop` — a pointer no caller supplies, can replace, or can invalidate.
None carried a `# Safety` section, and each body was already one `unsafe {}` with its proof, so the
marker was pure ceremony and its removal moves nothing.

All three live in files the workspace `deny` already covers (`video_vulkan.rs` since the fence came
off it), so they stay fully enforced.

Worth recording why this is where the marker sweep STOPS for these two crates: every remaining
candidate in pf-encode and pf-client-core sits inside a fenced file, and there removing a marker is
not free. The fence excuses `unsafe_op_in_unsafe_fn`, which only applies to `unsafe fn` BODIES — a
safe fn always needs explicit blocks — so unmarking an ash-dense helper forces exactly the
whole-body `unsafe {}` this program rejected when it rejected `cargo fix`. The way to reach those is
to reclaim the file first, not to unmark inside it.

Verified on .21: fmt + `clippy --workspace --all-targets -- -D warnings` + the feature-gated
`-p pf-encode` step, all rc=0.
2026-07-29 00:20:35 +02:00
enricobuehler ed4bbc6b0b refactor(client-core): the first fence comes off — video_vulkan.rs is at zero
`video_vulkan.rs` was fenced with the other thirteen GPU/FFI backends, but it never belonged with
them: its four sites are not ash calls, they are RAW POINTER DEREFERENCES inside the two FFmpeg
`lock_queue`/`unlock_queue` callback trampolines — exactly the case where the lint pays, and where a
proof carries an argument instead of restating a signature. Both bodies had none at all.

They keep `unsafe extern "C"`: FFmpeg invokes them with a `*mut AVHWDeviceContext`, which is a real
contract, now written down. What was missing is why the dereference chain is sound, and it is worth
stating because it is not obvious — the trampoline casts between pf_ffvk's `AVHWDeviceContext` and
ffmpeg-sys's (the same C struct declared twice), reads `user_opaque`, and dereferences it as a
`QueueLock`. That pointer borrows `VkCtxStorage::_queue_lock`, an `Arc<QueueLock>` whose field doc
already says it exists to outlive every call the context can make. The proof now connects those two
facts, so a future edit to the storage's lifetime has something to contradict.

With that, the file needs no exemption and the workspace `deny` covers it: 14 fenced files -> 13.
This is what removing a fence is supposed to look like — the sites that are worth narrowing get
narrowed, and the exemption disappears rather than being renewed.

Verified on .21: fmt + `clippy --workspace --all-targets -- -D warnings` + the feature-gated
`-p pf-encode` step, all rc=0 with no allow in the file.
2026-07-29 00:20:35 +02:00
enricobuehlerandClaude Opus 5 2a088e13a4 feat(client/windows): bind a host to a profile, connect with one, pin one, copy its link
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m8s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m37s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m25s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 3m43s
apple / swift (push) Successful in 5m17s
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
flatpak / build-publish (push) Canceled after 0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Canceled after 0s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
decky / build-publish (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
apple / screenshots (push) Canceled after 0s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
android / android (push) Canceled after 2m34s
ci / rust-arm64 (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bench (push) Canceled after 0s
ci / rust (push) Canceled after 2m30s
arch / build-publish (push) Canceled after 2m39s
The WinUI half of §5.2: `Target` carries a one-off profile into the session's `--profile`,
the host editor gains the binding picker, and the tile names its bound profile on a chip so
what a plain click will do is visible without opening anything.

Shape notes, both forced by this toolkit rather than chosen:

- The flyout has no submenus, so "Connect with", "Pin as card" and "Unpin card" are flat
  items, one per profile, matched by prefix. The prefixes end in ": " so a profile named
  "Copy link" can't collide with a fixed entry.
- The binding picker commits on change rather than at Save. The rest of that sheet is text
  boxes with draft refs, which need a Save; a ComboBox doesn't, and making it wait would be
  the odd one out.

Semantics stay identical to the Linux client, because they are the design's, not the
platform's: "Connect with" never rebinds, "Default settings" is `Some("")` so it really
does override a bound host for one session, and a binding whose profile was deleted reads
as Default settings and resolves to it.

`pf_client_core::clipboard::set_text` is the small addition the crate needed for "Copy
link" — the OS clipboard plumbing was already there, just private to the streaming bridge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 00:17:15 +02:00
enricobuehlerandClaude Opus 5 8299151a09 fix(client-core): drop nine GTK-client files a stray copy left in the crate
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m7s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m30s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m14s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m50s
apple / swift (push) Successful in 5m17s
android / android (push) Canceled after 5m13s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 4m47s
ci / bench (push) Canceled after 5s
ci / rust (push) Canceled after 3m13s
ci / rust-arm64 (push) Canceled after 3m12s
ci / web (push) Canceled after 34s
ci / docs-site (push) Canceled after 31s
decky / build-publish (push) Canceled after 0s
deb / build-publish-client-arm64 (push) Canceled after 0s
deb / build-publish (push) Canceled after 3s
deb / build-publish-host (push) Canceled after 0s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Canceled after 0s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
flatpak / build-publish (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 3s
An `rsync` with the wrong destination dropped the Linux shell's sources into
`crates/pf-client-core/src/` and they rode along with the previous commit. Nothing
referenced them — `lib.rs` never declared the modules, so they were dead weight rather
than a build break — but a crate that says it is UI-agnostic must not have a GTK shell
sitting in it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 00:04:57 +02:00
enricobuehlerandClaude Opus 5 ad99be2fb2 feat(client): the overlay learns absorb + clear, so per-control shells can edit profiles too
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m14s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m47s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m50s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m48s
ci / web (push) Successful in 1m8s
apple / swift (push) Successful in 5m16s
ci / docs-site (push) Successful in 1m4s
android / android (push) Failing after 9m5s
ci / bench (push) Successful in 5m34s
decky / build-publish (push) Successful in 21s
ci / rust-arm64 (push) Failing after 8m37s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 21s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
ci / rust (push) Failing after 9m28s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 20s
deb / build-publish-host (push) Successful in 10m37s
deb / build-publish-client-arm64 (push) Successful in 9m38s
arch / build-publish (push) Successful in 17m48s
deb / build-publish (push) Successful in 13m50s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 6m33s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11m9s
flatpak / build-publish (push) Failing after 9m27s
apple / screenshots (push) Canceled after 0s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 6m9s
Groundwork for the Windows half of P1, and a dedup of the Linux one.

The two shells commit differently: GTK writes once when the dialog closes (so it can hand
over a list of touched rows), WinUI writes on every control change (so it can't). `absorb`
serves the second shape — give it the effective settings before and after one control
fired, and it records the field that moved.

That is deliberately NOT the diff-on-save the design rejects. The comparison is against
the EFFECTIVE settings, i.e. what the control was actually showing, not against the
globals — so setting a value back to whatever the global happens to be still records an
override, which is exactly the pin the design wants. It only ever adds; removing an
override stays an explicit, separate operation.

`clear` is that operation, keyed by the overlay's own field names, with `resolution` as
the one alias for the width/height/match-window tri-state a single control drives on every
client. The GTK reset path now calls it instead of carrying its own `match` — that second
list was already a place where the two could drift, and the next client would have made it
a third.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 00:04:26 +02:00
enricobuehler 6de325a6b6 fix(ci): the unsafe lint said warn while CI enforced it as deny, and main went red
windows-host / package (push) Failing after 13m8s
windows-host / winget-source (push) Skipped
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m43s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m31s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m48s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m55s
ci / web (push) Successful in 1m21s
ci / docs-site (push) Successful in 1m5s
android / android (push) Failing after 6m25s
ci / bench (push) Successful in 8m21s
deb / build-publish (push) Successful in 8m44s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
decky / build-publish (push) Successful in 32s
deb / build-publish-host (push) Successful in 10m2s
ci / rust (push) Failing after 17m18s
ci / rust-arm64 (push) Successful in 16m6s
arch / build-publish (push) Failing after 17m19s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 24s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 7m54s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7m44s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11m12s
deb / build-publish-client-arm64 (push) Successful in 14m5s
flatpak / build-publish (push) Failing after 8m59s
apple / swift (push) Failing after 7m20s
apple / screenshots (push) Skipped
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 17m16s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 26m3s
`unsafe_op_in_unsafe_fn = "warn"` was adopted workspace-wide in 39513528 on the assumption that
`warn` is a soft setting you can clear at leisure. It is not: ci.yml runs `cargo clippy … -D
warnings`, which promotes it to a hard error, so main has failed on EVERY commit since — Linux
`rust` and `rust-arm64` both dying on `pf-client-core` with 70 E0133 errors, and windows-host.yml
alongside them. A lint level that understates its own severity is worse than a strict one, so this
states what CI already does — `deny` — and writes the exemptions down instead.

Fourteen GPU/FFI backend files take `#![allow(unsafe_op_in_unsafe_fn)]`, each with its reason and
the workspace Cargo.toml carrying the argument in full. They are not "not done yet": measured
across them, 64% of the sites are a single third-party FFI call (ash, pyrowave-sys, libav, the
NVENC/AMF entry tables), and of the 44 `unsafe fn`s only 4 have a body containing no unsafe
operation at all. Since pf-encode also denies `undocumented_unsafe_blocks`, narrowing them means a
hand-written SAFETY comment per line that could only restate the signature — the exact noise that
made `unsafe` stop meaning anything here before. Everything else stays at zero and enforced, and
each allow is removable on its own terms.

Two smaller things this had to clear, both invisible to the job that would have caught them:

- `service.rs`: `undocumented_unsafe_blocks` wants the proof on EACH block, and a comment covering
  a group of consecutive `unsafe` statements only credits the first — so the two `OwnedHandle`
  wraps became their own statements. Windows-gated, so only the .47 gate sees it. 37a42078 fixed
  the same class of failure in pf-frame/dxgi.rs concurrently; that fix is taken as-is here.
- `clients/linux/ui_settings.rs`: `show`'s deletion left its doc comment stranded on `show_scoped`,
  which silently merged two unrelated doc blocks onto one fn. Removed.

Verified by running the real CI commands, not proxies: on .21 fmt + `clippy --workspace
--all-targets -- -D warnings` + the feature-gated `-p pf-encode --features
nvenc,vulkan-encode,pyrowave` + build + test, all rc=0; on the Intel box .47 all four
windows-host.yml clippy invocations, all rc=0. The arm64 job lints pf-client-core and
punktfunk-client-linux, both covered here.
2026-07-28 22:32:30 +02:00
enricobuehler 06a406adf2 refactor(client): the D3D11VA decoder owns its refs too, and the owner-only fields say so
Completes the decoder trio. `D3d11vaDecoder::new` had the same cascading unwind as
the VAAPI and Vulkan constructors — four branches each unref'ing the hwdevice by
hand — and `d3d11va_decode_supported` opened a frames context it released on one
exit path while relying on the null check for the other. Both are `AvBuffer` now.
The one surviving `av_buffer_unref` in the file is deliberate: it runs before
ownership is taken, on the `av_hwdevice_ctx_init` failure ahead of `from_raw`.

The Windows gate then caught something worth keeping: `field hw_device is never
read`, in all three decoders. It was true and it was new — the hand-written `Drop`
used to be the field's only reader, so moving the unref into the type left a field
that is *purely* an owner. The compiler cannot express that, so each one now
carries a doc comment saying what it is and an explicit `#[allow(dead_code)]`.
Deleting the field would free the device early; renaming it `_hw_device` would
silence the lint by hiding what it holds. Neither is what we mean.

Verified on BOTH platforms this time. Windows runner .133 (the toolchain env from
the runner notes, plus `PF_FFVK_VULKAN_INCLUDE` for pf-ffvk): `cargo check -p
pf-client-core --all-targets` at EXITCODE=0, clean of the dead-code warnings. Linux
.21: `cargo check --workspace --all-targets` exit 0 / zero errors, pf-client-core
34 passed / 0 failed, pf-encode 33 passed / 0 failed.
2026-07-28 21:31:41 +02:00
enricobuehler a42ab075a8 refactor(client): the hardware decoders own their hwdevice ref
`VaapiDecoder::new` and `VulkanDecoder::new` create a hwdevice and then do several
more fallible things with it — resolve `vkWaitSemaphores`, find the decoder, alloc
the context, open it — and every one of those branches unref'd the device by hand,
with a `Drop` doing it once more. Six hand-written unrefs between them, each one a
line somebody has to remember when adding a step.

`video_libav::AvBuffer` owns it instead, so an early `bail!` releases it and the
branches carry nothing. It is a deliberate second copy of `pf-encode`'s type: the
crates do not depend on each other (host encode and client decode share no code
path), and this one needs something the host's does not — see below — so hoisting
it to a shared crate would mean giving that crate an ffmpeg dependency and both
sets of semantics to save about twenty lines.

That extra piece is `into_raw`. `pick_vulkan` hands its frames context to the codec
(`(*ctx).hw_frames_ctx = fr` — the codec unrefs it when the context closes), so the
wrapper must give up ownership rather than drop. Making the transfer explicit is
the point: dropping an `AvBuffer` there too would be exactly the double-unref this
type exists to prevent.

Two `av_buffer_unref` calls survive in `video_vulkan.rs` on purpose. One runs before
ownership is taken (the `av_hwdevice_ctx_init` failure, ahead of `from_raw`); the
other releases the codec's OWN pre-existing frames ctx before we replace it, which
was never ours to model.

Field order preserved: `hw_device` stays declared after `ctx`, so it still releases
after each `Drop` frees packet/frame/context — the order the hand-written unref had.

Verified on .21 (CachyOS, FFmpeg 62): `cargo check --workspace --all-targets` clean
at exit 0 with zero errors — the workspace-wide check owed since the AvBuffer
commit — plus pf-client-core 34 passed / 0 failed and pf-encode 33 passed / 0
failed. The decoders themselves still need real VAAPI/Vulkan playback to exercise.
2026-07-28 21:31:41 +02:00
enricobuehlerandClaude Opus 5 eab4829630 feat(client): the brain layer — one connect plan, one wake machine, one spawn
C0 of design/client-architecture-split.md. Wake-then-connect exists three times today —
GTK's `WakeConnect`/`wake_fallback`, the WinUI shell's `wake_and_connect` (whose comment
says it "mirrors the Apple HostWaker"), and Apple's `HostWaker` itself — and the
deep-link and profile work was about to make it five. `pf-client-core` already held the
ingredients (wol, discovery, trust, library, session); what was missing was the layer
that composes them, so every front-end composed them itself.

`ConnectPlan` is a resolved intent: which host, which pin, which launch id, which
profile, the effective settings, whether to wake. One constructor per door — a card
click, a CLI verb, a URL — one type out. `ConnectPlan::resolve` is pure (stores in,
plan out), which is what lets the URL router be tested without a config directory;
`for_host` is the convenience that loads them.

`plan_from_link` is where the deep-link security rules actually live, rather than in
each shell: a contradicted fingerprint refuses and says which host it was about, an
ambiguous name refuses instead of picking the first, a profile the catalog can't honor
refuses BEFORE anything is dialled, and an unknown — or known-but-never-pinned — host
becomes a confirmation sheet carrying the claimed name and expected pin, so the first
connect is verified rather than blind TOFU. Preempting a live session stays with the
caller: only the front-end knows one is running.

`WakeWait` is Apple's `HostWaker` cadence as a pure step function — packet at 0 s and
every 6 s, presence polled every second, 90 s budget, and a PARK (not an error) at the
end, because "it didn't wake in 90 s" is usually "give it 10 more". As a step function
each front-end drives it from its own loop and they still agree, and the whole cadence
is testable without waiting 90 seconds.

Session spawn, its argv and its stdout contract move here too, and the GTK shell now
spawns through them — so "which flags does a stream get" and "what does ready mean" have
one answer for the shells, the console and the coming CLI. `--profile` deliberately does
NOT ride a plain card click: the session resolves the host's binding through the same
helper, and passing it would be a second source of truth for one decision.

Not yet adopted: the two shells' wake paths. That swap changes the most-used path in the
product and the handoff wants it verified on glass with a genuinely sleeping host, which
this session couldn't do — so the state machine lands, the duplication stays until it
can be verified away.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 20:45:07 +02:00
enricobuehlerandClaude Opus 5 3350d3cfbf feat(client): the punktfunk:// grammar — one parser, one emitter, one vector file
D0 of design/client-deep-links.md. Apple has shipped `punktfunk://connect/<uuid>` for a
while; this is the same grammar, generalised and given a Rust implementation the GTK and
WinUI shells, the session and the coming CLI all share:

    punktfunk://connect/<host-ref>[?fp=…][&host=addr[:port]][&launch=…][&profile=…][&name=…]

The rule the grammar exists to keep is that a URL may only ever do what a click on an
existing card could do, minus trust decisions. So it carries REFERENCES to things that
already exist on this device — never values: no resolution, no bitrate, no codec, so a
web page cannot shape a session beyond picking among the user's own configurations. And
`pair` is not a route: it parses, and refuses, so a link can never start a trust
ceremony. Everything hostile is rejected here, once, for every front-end — 2 KB total
cap, per-parameter caps, strict percent-decoding (a half-escape or invalid UTF-8 is an
error, not a shrug), control characters refused after decoding, launch ids held to the
charset the host and Decky already agree on, and `fp=` that must actually be 64 hex.

Resolution follows the same order everywhere: stable record id → unique host name →
`addr[:port]`, with `host=`+`fp=` as the reinstall recovery path, which is why
self-emitted links carry all three. An ambiguous name refuses rather than guessing; a
stale id with no recovery address refuses rather than dialling "11111111-…" as if it
were a hostname; an unknown-but-plausible address becomes the confirmation sheet, never
an auto-connect.

`pf://` parses as an input alias and is never emitted or registered — claiming a
two-letter scheme on MSIX/Apple is unconditional squatting, and a link that resolves on
one platform only is a trap.

The 44-case `clients/shared/deeplink-vectors.json` is the cross-language contract: this
module's tests run every case, and the Swift and Kotlin ports will run the same file, so
three parsers cannot drift into three different security postures. Refusal codes are
part of that contract, not just the happy path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 20:32:32 +02:00
enricobuehlerandClaude Opus 5 a2be3f4de9 feat(client): settings profiles — the override catalog, the one resolver, and the session flag
A client setting is global today: pick 4K@120 HDR and the retro box gets it too.
The one escape hatch (per-host `clipboard_sync`) proves the shape works but covers a
single field. This is P0 of design/client-settings-profiles.md — the headless core the
shells, Apple, Android and the deep-link grammar all build against. No UI yet.

A profile is a NAMED BUNDLE OF OVERRIDES, not a snapshot: `SettingsOverlay` is sparse
`Option`s, so a field the user never touched keeps following the global value live, and
fixing a global once fixes it everywhere. `Some(x)` equal to today's global is still
meaningful — it pins x against a later global change — which is why the UI will write
`Some` on touch and `None` on an explicit reset, never by diffing.

The catalog is its own `client-profiles.json`, deliberately not part of the settings
file: that file has five whole-file load-modify-save writers with no merge, so a profile
written by one would be dropped by the next. Which host uses which profile is a field on
the host record instead of a map keyed by host — that is what dissolves the client's
long-standing "what identifies a host record" question for this feature.

Resolution has exactly one implementation, `trust::effective_settings()`:

    effective = overlay(profile).apply(global)
    profile   = one-off pick ?? host binding ?? none

Both session construction sites go through it (`main.rs` AND `console.rs` — touching one
and not the other is a Windows-only build break), so the console, and therefore Decky,
honor host bindings with no work of their own. Nothing here can fail a connect: a
deleted binding resolves as "no profile", and a one-off that can't be honored falls back
to the DEFAULTS rather than to the host's own profile — "connect with Work" must never
quietly stream "Game". `--profile <id|name>` is the one-off door for the shells' coming
"Connect with ▸" and for `punktfunk://…&profile=`; `--profile ""` forces the defaults on
a bound host. The active profile's name closes the stats overlay's first line, so
"which profile am I on?" is answerable without leaving the stream.

Also here, because this effort touches every one of these anyway:

- `KnownHost` gains `profile_id`, `pinned_profiles` and a lazily minted stable `id`. The
  id is groundwork (design §4.5) — no lookup is re-keyed, `fp_hex`/`addr:port` stay.
- `upsert` now preserves the state the USER set on a record — the profile binding, its
  pins, the clipboard decision, the id — against refreshes that carry none of it.
  `clipboard_sync` survived that only because the function happened not to mention it.
- `KnownHost::default()` exists so construction sites read `..Default::default()`; five
  hand-written literals were exactly how a new field gets silently dropped on re-pair.
- All three client stores now write temp+rename. A torn settings file loads as `Default`
  — i.e. silently resets every setting the user has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 20:22:33 +02:00
enricobuehlerandClaude Opus 5 1ee06defa6 feat(library): descriptive metadata on every entry — platform, year, genres, and friends
apple / swift (push) Successful in 5m38s
windows-host / package (push) Successful in 19m0s
ci / web (push) Successful in 56s
ci / docs-site (push) Successful in 1m4s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m15s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m21s
ci / bench (push) Successful in 5m41s
ci / rust-arm64 (push) Successful in 9m32s
ci / rust (push) Failing after 12m19s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m46s
android / android (push) Successful in 17m49s
decky / build-publish (push) Successful in 1m11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 16s
arch / build-publish (push) Successful in 19m46s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 39s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 22s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 5m22s
deb / build-publish (push) Successful in 9m9s
deb / build-publish-client-arm64 (push) Successful in 7m26s
deb / build-publish-host (push) Successful in 9m46s
flatpak / build-publish (push) Failing after 8m33s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m57s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 21m56s
docker / build-push-arm64cross (push) Successful in 9s
docker / deploy-docs (push) Successful in 11s
apple / screenshots (push) Successful in 25m51s
Emulation-and-beyond libraries need more than a title and a poster. Every
library shape (GameEntry, CustomEntry, CustomInput, ProviderEntryInput)
now carries a shared, flattened GameMeta: platform, description,
developer, publisher, release_year, genres, tags, region, players. All
fields are optional and flat on the wire, so existing library.json files,
provider plugins, and clients keep working unchanged.

- Installed-store scanners (Steam, Lutris, Heroic, Epic, GOG, Xbox) stamp
  platform=PC; custom/provider entries carry whatever was authored.
- GET /library grows a ?platform= filter (case-insensitive) beside
  ?provider=.
- Console: the add/edit form gets a Details section (round-tripping every
  field, since update replaces the entry), the poster tile a platform
  badge (non-PC only — the store badge already implies PC) and the year.
- plugin-kit: ProviderEntry accepts the same fields (new GameMeta schema,
  spread flat); SDK + OpenAPI spec regenerated.
- pf-client-core decodes platform for future client badges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:00:30 +02:00
enricobuehlerandClaude Opus 5 6edd700910 fix(client/gamepad): honor an explicit controller type — the per-pad arrival was re-declaring the physical pad
The "Controller type" setting reached the Hello and stopped there. Every pad then
opened with a GamepadArrival carrying its DETECTED kind, and the host builds each
virtual device from that arrival — the session default is only the fallback for a
pad that never declares one. So "emulate my DualSense as a DualShock 4" put a
DualSense on the host the instant the controller connected, and no amount of
reconnecting helped. Apple and the native clients both; Android already applied
the setting per pad.

The setting now rides the declaration too: explicit wins for every slot, Automatic
keeps per-pad detection so a mixed session stays honest. The physical kind still
drives the LOCAL feedback paths — a DualSense emulated as a DualShock 4 keeps its
lightbar and adaptive triggers, and a Deck keeps its rumble keep-alive.

Regressed in 97c67b26 / 76be4c3e (multi-controller support); ships in 0.19.x.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 16:56:44 +02:00
enricobuehler 94b818da28 fix(client/pyrowave): declare the decode planes' real format — 10-bit sessions decoded through an R8 view of R16 planes
apple / swift (push) Successful in 5m14s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 6m32s
arch / build-publish (push) Failing after 7m6s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
decky / build-publish (push) Successful in 18s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m53s
android / android (push) Successful in 12m56s
docker / deploy-docs (push) Successful in 24s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m41s
docker / build-push-arm64cross (push) Successful in 4m15s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m40s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 7m44s
deb / build-publish-host (push) Successful in 10m43s
deb / build-publish-client-arm64 (push) Successful in 9m8s
apple / screenshots (push) Successful in 22m58s
flatpak / build-publish (push) Successful in 6m56s
deb / build-publish (push) Successful in 8m58s
ci / rust (push) Successful in 22m10s
ci / rust-arm64 (push) Successful in 9m22s
ci / web (push) Successful in 48s
ci / docs-site (push) Successful in 1m13s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 12m48s
ci / bench (push) Successful in 6m3s
The HDR/4:4:4 leg (188edde2, shipped 0.14.0) taught the client's plane
ring to allocate R16_UNORM on a 10-bit session, but the
pyrowave_image_view structs handed to decode_gpu_buffer kept the
original SDR shape: image_format/view_format hardcoded R8_UNORM, chroma
extents hardcoded half-res. pyrowave wraps our VkImage under the
DECLARED format and vkCreateImageView's its storage view from it
(pyrowave_c.cpp WrappedViewBuffers::wrap) — an R8 view over a
non-MUTABLE R16 image is invalid Vulkan that release drivers execute
anyway: the view's addressing covers half the surface, so the iDWT's
8-bit codes fuse pairwise into 16-bit texels (structured tile-scrambled
garbage) and the never-written remainder samples as all-plane zeros
(saturated green). Field report 2026-07-25: RX 9070 XT host → 780M
client, total corruption on every 10-bit PyroWave session regardless of
bitrate/refresh, wire counters spotless; milder shapes of the same UB
on NVIDIA clients; Arc validation never saw it because it ran SDR
(R8 ring — declaration consistent by accident).

Declare what the ring really built: format from hdr16 (R16/R8), chroma
extents from chroma444 (full-/half-res) — exactly what upstream's own
pyrowave_image_get_image_view fills from the image itself. Host
encoders audited clean: Windows keys plane formats off hdr16 already,
Linux is 8-bit end to end.
2026-07-25 21:39:30 +02:00
enricobuehlerandClaude Opus 5 915fc3ef9e feat(session): headless --pair, so a box with only SSH can enrol
`punktfunk-session --pair <PIN> --connect host[:port]` runs the SPAKE2
ceremony with no window and no toolkit, prints the same
`paired <addr>:<port> fp=<hex>` line as `punktfunk-client --pair`, and
exits. Until now the PIN ceremony lived only in the GTK shell or the Skia
console, so enrolling an embedded/kiosk client meant installing a desktop
on it — or copying the identity store by hand.

Dispatches above every graphics call (the machine may have no display) and
is in the `--no-default-features` build: enrolling must never be the reason
a minimal image pulls in Skia. `--name` defaults to the hostname instead of
the desktop path's hardcoded "Steam Deck".

`forget_placeholder` and `device_name` move into pf_client_core::trust so
the two binaries share one implementation rather than the session
re-deriving them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:32:45 +02:00
enricobuehlerandClaude Fable 5 4a01bc4463 feat(core+host+client): cursor channel — remote-desktop sweep M2a+M2b
The host cursor stops riding the video and becomes a real OS cursor on
the client (the Parsec/RDP model): pointer feel no longer pays the
capture→encode→network→decode→present round trip.

Wire (M2a):
- Hello grows a client_caps trailing byte (CLIENT_CAP_CURSOR) after the
  fixed display_hdr block — presence disambiguated by remaining length,
  which caps the post-HDR tail at 27 bytes (documented); Welcome answers
  HOST_CAP_CURSOR (capable-and-asked, the 444/clipboard precedent).
- CursorShape (0x50, control stream): serial + dims + hotspot + straight
  RGBA, ≤120px/side so the u16 frame always fits (128² would overshoot);
  client caches by serial — re-showing a known shape costs 14 bytes, not
  a bitmap (RDP pointer-cache for free).
- CursorState (0xD0 datagram): serial + visible/relative_hint flags +
  position, sent once per encode-loop tick — latest-wins, self-healing
  under loss, no refresh timer. relative_hint is reserved for M3.
- Client core: two new planes (control-task + datagram-task arms) →
  next_cursor_shape/next_cursor_state; connect() grows client_caps
  (C ABI passes 0 until the v11 cursor poll fns exist).

Host (M2b, Linux portal only):
- handshake::cursor_forward is THE predicate (client asked ∧ Linux ∧
  compositor ≠ gamescope) — Welcome bit and session wiring both read it.
- SessionPlan.cursor_blend goes false for a forwarding session; the
  encode loop ticks a CursorForwarder every iteration: shape-serial diff
  → control-task bridge (mirrors probe_result), state datagram → conn.
- CursorOverlay/capture CursorState carry the hotspot through
  (nearest-neighbor downscale backstop for XL cursors, unit-tested).

Presenter:
- CursorChannel drains both planes per loop iteration; shapes become
  SDL color cursors (from_surface + hotspot), applied while the desktop
  mouse model is engaged; visibility follows the host; capture/released
  hands back the system cursor. Sessions advertise the cap when they
  START in desktop mode.

Verified on .21: fmt + clippy -D warnings (7 crates) + tests green
(core 218 incl. new wire roundtrips, host 245 incl. e2e + forwarder
downscale tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Fable 5 df04880273 feat(client/input): desktop (absolute) mouse mode — remote-desktop sweep M1
New physical-mouse model beside capture: Desktop leaves the pointer
uncaptured and sends absolute positions through the letterbox
(MouseMoveAbs — every host injector already consumes it). Capture
stays the default and the game model.

- pf-client-core: MouseMode (capture|desktop) persisted in Settings,
  default capture so existing stores are unchanged.
- pf-presenter: Capture grows a desktop model — latest-wins pending
  abs position coalesced per loop iteration (same 1000 Hz discipline
  as relative), flushed before clicks/keys/wheel so they land where
  the cursor is; Ctrl+Alt+Shift+M flips the model live; local cursor
  stays hidden over the window (the host's composited cursor is the
  one you see until the M2 cursor channel); Windows keyboard grab
  only engages for capture (a desktop stream is something you
  Alt-Tab away from).
- gamescope gating: its EIS is relative-only, so desktop mode is
  pinned off there (resolved_compositor), with a log note.
- Settings surfaces: GTK row (dynamic caption), WinUI combo,
  console-UI row + step test, capture-hint line.

Verified: fmt + clippy -D warnings + tests (33/40/19) on Linux .21;
clippy -D warnings for all five crates incl. punktfunk-client-windows
native on .173.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 23:37:32 +02:00
enricobuehlerandClaude Fable 5 256aa5f7f2 fix(steamdeck): install the plugin runner + harden Deck pad typing + repair update.sh
- scripts/steamdeck/install.sh (§2b) + update.sh: build and install the
  scripting runner user-scoped (~/.local wrapper + pinned bun + bundle) —
  SteamOS's read-only /usr can't take the .deb layout, so the console's
  plugin store reported "the plugin runner isn't installed" on every
  SteamOS host. update.sh retrofits it onto existing installs.
- plugins.rs runner discovery: check the ~/.local layout after the /usr
  ones; mention the SteamOS path in the not-installed error.
- update.sh: define warn() — it was used but never defined, so under
  `set -e` the first warn aborted the update before services restarted.
- pf-client-core gamepad: a Steam Input virtual pad forwarded on a real
  Deck (the only-pad case) now declares the DECK kind in its per-pad
  arrival instead of the wrapper's Xbox 360 identity — the session-level
  auto_pref already resolved SteamDeck, but the arrival overrides it on
  current hosts.
- reject tests: cover SetupFailed; move the foreign-code probe off 0x68.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 10:01:54 +02:00
enricobuehlerandClaude Fable 5 2562663fc6 fix(host/linux): survive and self-heal dead gamescope sessions on headless SteamOS boxes
Three fixes for the "most connects simply fail" trap on a SteamOS host
with no physical display (VM testbox, panel-less mini PC):

- restore path: skip the restore-to-physical-panel when no DRM connector
  reports a connected display — removing the headless drop-in and
  restarting gamescope-session.target on such a box just crash-loops
  gamescope and strands every later connect on "no usable compositor".
  Keep the headless session (and the takeover state) instead.
- connect path: when no live graphical session exists but the MANAGED
  gamescope infra is present (SteamOS gamescope-session / Bazzite
  session-plus), route to the gamescope takeover — which rebuilds the
  session at the client's mode — instead of failing the connect.
- error delivery: a session-setup failure used to just drop the QUIC
  connection, reaching the client as "control stream finished mid-frame"
  (indistinguishable from transport trouble). Close with a typed
  setup-failed code (0x68, RejectedSetupFailed = -29) carrying the error
  text, and give the client handshake/pairing error paths a short grace
  for the CONNECTION_CLOSE to beat the stream error it caused.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 09:50:30 +02:00
enricobuehlerandClaude Fable 5 a784682d4c fix(client/net): split the receipt stamp from the pull + bleed standing latency
ci / docs-site (push) Successful in 56s
apple / swift (push) Successful in 1m14s
ci / web (push) Successful in 2m30s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
ci / bench (push) Successful in 5m57s
release / apple (push) Successful in 9m8s
deb / build-publish (push) Successful in 9m33s
arch / build-publish (push) Successful in 13m14s
docker / deploy-docs (push) Successful in 23s
flatpak / build-publish (push) Failing after 8m1s
deb / build-publish-host (push) Successful in 10m24s
android / android (push) Successful in 14m56s
apple / screenshots (push) Successful in 6m43s
ci / rust (push) Successful in 19m29s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m35s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m29s
windows-host / package (push) Successful in 15m13s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 5m48s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 6m38s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 7m45s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 8m51s
The two-pair investigation (wired Mac clients stuck at a rock-steady
~18-19 ms "network" that survived the load ending and cleared only on
reconnect) exposed two structural gaps, one of measurement and one of
recovery:

- Receipt was stamped at the hand-off PULL (Swift nextAU, pf-client-core,
  Android decode loops), not at reassembly completion — so any client-side
  standing state between the reassembler and the pull read as NETWORK
  latency, undiagnosable from the HUD. ABI v9: `PunktfunkFrame`/`Frame`
  grow `received_ns`, stamped by `Session::poll_frame` as the AU crosses
  the session boundary. Every embedder now uses the core stamp; the Apple
  client keeps the pull instant as `AccessUnit.pulledNs` and shows the
  receipt→pull wait as its own "client queue" term (detailed HUD tier from
  2 ms + a `queue_p50` stats-log field). Decode stages keep their pull
  anchor on all platforms, so no historical stage shifts meaning.

- The jump-to-live detectors deliberately ignore anything under 6 queued
  frames / 400 ms behind — so a small, constant, loss-free elevation (a
  sub-frame standing backlog, or a stale clock offset after a wall-clock
  step/slew) is carried for the rest of the session. New third detector
  (`StandingLatency`, unit-tested ladder): window-MIN one-way delay
  ≥ 10 ms above the session floor with zero loss for ~4.5 s escalates
  gently — a free clock re-sync first (an applied re-sync re-bases the
  floor), then at most 3 flush+keyframe bleeds sharing the jump-to-live
  cooldown, then a loud disarm naming what it means. Loss windows reset
  the run: congestion belongs to FEC/ABR, not this detector.

Also: mid-stream re-sync apply/discard logs debug→info — they are the
forensic trail for the stale-offset case and were invisible in the field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 23:38:11 +02:00
enricobuehlerandClaude Fable 5 40c2f68231 feat(client): device-pick plumbing — GPU enumeration + audio endpoint targeting
The Settings GPU pick existed (adapter → PUNKTFUNK_VK_ADAPTER) but no Linux
shell could enumerate anything to pick: the GTK shell deliberately links no
Vulkan. pf_presenter::vk::list_adapters() reads the physical devices'
marketing names (no surface, discrete first, deduped — the name is the whole
match key in pick_device), surfaced as `punktfunk-session --list-adapters`.

Audio gets the same treatment for the new speaker_device/mic_device settings
(PipeWire node.name; empty = default): session main maps them onto
PUNKTFUNK_AUDIO_SINK/SOURCE — a hand-set env still wins, like the adapter —
and the playback/mic streams pass them as `target.object` (raw key: the
keys::TARGET_OBJECT constant is feature-gated on a newer libpipewire than we
require). pf_client_core::audio::devices() is the registry roundtrip the
pickers read, exposed for debugging as `punktfunk-session --list-audio`.
The WASAPI leg (Windows endpoint IDs) is still to come; the fields are
ignored there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:32:38 +02:00
enricobuehlerandClaude Fable 5 8fe90a8a4b fix(client): clipboard poll cadence was never applied (and CI clippy)
ci / web (push) Successful in 1m8s
apple / swift (push) Successful in 1m17s
ci / docs-site (push) Successful in 1m9s
apple / screenshots (push) Successful in 6m17s
ci / bench (push) Successful in 6m51s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
deb / build-publish (push) Successful in 9m11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
android / android (push) Successful in 13m32s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8m1s
arch / build-publish (push) Successful in 17m21s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Failing after 5m6s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7m43s
deb / build-publish-host (push) Successful in 11m42s
flatpak / build-publish (push) Failing after 8m23s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Failing after 6m14s
ci / rust (push) Successful in 25m38s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 5m45s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m54s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m32s
docker / deploy-docs (push) Successful in 27s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 9m8s
POLL was dead: the local clipboard was re-read once per inbound event wait
(<=120 ms) instead of on its own 400 ms cadence, so the constant documenting
the interval described something the code did not do. Give it a deadline of
its own -- the event wait is short because it bounds teardown latency, which
is no reason to hammer the Win32 clipboard eight times a second while the
user is copying in another app.

Build State in one expression while here, and note for next time: CI runs
clippy as --workspace --all-targets -- -D warnings, so a scoped run without
-D warnings (what I did) does not reproduce it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:22:24 +02:00
enricobuehlerandClaude Fable 5 aa45757a72 feat(client/windows): per-host clipboard toggle
The bridge landed always-on whenever the host permitted it; sharing a
clipboard is a trust decision about a specific host, so it needs to be
opted into. Mirrors the Apple client's per-host model
(StoredHost.clipboardSync, "Share clipboard with this host") rather than a
global switch: KnownHost::clipboard_sync, toggled from the host card's
overflow menu, default off.

The session binary resolves the stored flag itself in session_params, so a
direct connect and the console's own launches honor the same decision
without every caller having to remember to pass it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:06:06 +02:00
enricobuehlerandClaude Fable 5 cab6350723 feat(client): shared clipboard on Windows
The protocol half has been in punktfunk-core since the clipboard work
landed -- the per-session fetch task, plus clip_control/clip_offer/
clip_fetch/clip_serve/next_clip on NativeClient -- but only the Apple
client ever drove it, through the C ABI. The Windows and Linux clients link
the core directly and simply never wired it, so copy-paste between host and
client worked on a Mac and nowhere else.

This adds the missing half: the OS-clipboard bridge, on its own session
thread beside the audio one.

Local -> remote stays lazy by construction, as the design asks: a
GetClipboardSequenceNumber poll spots a local copy and we announce only the
FORMAT LIST; bytes are read when (and only when) the host actually pastes
and sends a FetchRequest.

Remote -> local is EAGER in this first cut, which is a deliberate deviation
worth naming. macOS gets laziness free from NSPasteboardItemDataProvider;
the Windows equivalent is delayed rendering, which needs a clipboard-owning
window running its own message pump. So we fetch on the offer and place real
bytes under a 4 MB cap -- text always crosses, a large image is skipped
rather than pulled for a paste that may never come. Delayed rendering lifts
the cap later.

Echo suppression is the design's Windows rule verbatim (capture the
sequence number right after our own SetClipboardData); without it every
copy ping-pongs between the machines forever. Content marked
ExcludeClipboardContentFromMonitorProcessing -- what password managers set
-- is never announced and never served.

Text and PNG for now. Apps that publish only CF_DIB need the conversion the
host already has. Linux keeps a stub: the bridge itself is
platform-neutral and will drive a data-control seam unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:06:06 +02:00
enricobuehlerandClaude Fable 5 51f7ff1b3f feat(client/windows): invert scroll direction
The Apple client's toggle, wired through the shared session presenter:
Settings::invert_scroll -> SessionOpts -> Capture, applied at the single
seam where wheel deltas enter (before accumulation, so the fractional
remainders stay consistent with what was actually sent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:06:06 +02:00
enricobuehlerandClaude Fable 5 7a1809547b fix(client/windows): left-align setting captions, drop the width cap, add auto-wake
Three follow-ups to the parity sweep:

- Captions read centred: a TextBlock defaults to Stretch alignment, so the
  MaxWidth cap left it floating in the middle of the leftover width instead
  of sitting under its control. Pin them (and the footers) Left.
- The settings column kept the shared page() 640px cap, which -- next to the
  NavigationView pane spending the left third -- squeezed the cards into a
  narrow ribbon. Drop the cap here.
- Auto-wake on connect (the Apple client's toggle) is now a setting rather
  than unconditional behavior. Default on, which is what it did before.
  Gating the flow's ENTRY, not its packet sends: with it off a failed dial
  just fails, instead of showing a "Waking..." wait that would never send a
  packet to end itself. The explicit "Wake" host-card menu action is
  deliberately NOT gated -- that is the user asking directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:06:06 +02:00
enricobuehlerandClaude Fable 5 1ef0229bd9 feat(client/windows): PyroWave decode + surface it in the GUI codec picker
The decoder was gated to Linux because, when it landed, the Windows client
still had its own in-process WinUI/D3D11 presenter and the PyroWave present
path there was an open question. That client has since been retired: Windows
now spawns the SAME Vulkan session presenter as Linux, and the decoder is
plain Vulkan compute on the presenter's device (no fds, no dmabuf, no D3D11
interop), so the question that gated it answered itself. pyrowave-sys already
builds on Windows too -- the Windows HOST encoder ships on it.

So this is a port by un-gating: every cfg(all(target_os = "linux", feature =
"pyrowave")) becomes any(linux, windows) -- decoder module, backend variant,
Decoder::new_pyrowave, the CODEC_PYROWAVE advertisement, the session pump's
opt-in/build/label arms, and the presenter's planar CSC pass. No new code.

Then offer it in the Windows GUI, which is what prompted this. It stays
preference-only (resolve_codec never auto-picks it) and a host or device that
can't do PyroWave just falls back down the ladder to HEVC.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:06:06 +02:00
enricobuehlerandClaude Fable 5 1d587a259e fix(client/d3d11va): green bar at the bottom — clamp the blit to the frame
ci / web (push) Successful in 47s
ci / docs-site (push) Successful in 1m8s
apple / swift (push) Successful in 1m18s
decky / build-publish (push) Successful in 23s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 13s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 6m49s
ci / bench (push) Successful in 7m2s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 5m4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
apple / screenshots (push) Successful in 6m12s
deb / build-publish (push) Successful in 9m49s
android / android (push) Successful in 13m29s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 6m53s
docker / deploy-docs (push) Successful in 27s
flatpak / build-publish (push) Successful in 6m32s
deb / build-publish-host (push) Successful in 13m26s
arch / build-publish (push) Successful in 17m13s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 8m0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m10s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 21m2s
ci / rust (push) Successful in 28m51s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 9m0s
The decode surface is DXVA-aligned (height rounded up to the profile's
alignment, 128 for HEVC/AV1), so it is taller than the frame: a 2400-line
stream decodes into a 2432-line texture. VideoProcessorBlt was called with
no stream source rect, so it blitted the WHOLE surface — the uninitialized
padding rows (NV12 Y=0,U=V=0, which converts to vivid green) landed at the
bottom of the output and the picture was squashed to fit. Set the source
rect to the real frame.

Pre-existing on this backend, but 40030e90 made D3D11VA auto's first choice
on Intel, so it went from a corner case to what every Intel user sees — a
~32 px green bar at 3840x2400, reported on glass within the hour.

Also dedupe the Settings GPU picker by description: that string IS the
identity downstream (persisted as Settings::adapter, matched by name as
PUNKTFUNK_VK_ADAPTER), so repeated enumerations of one adapter offered the
user the same choice twice — live on an Intel Arc laptop, whose Vulkan ICD
also enumerates the single physical iGPU twice. Both paths now log their
raw enumeration (decode texture dims; DXGI name/LUID/ids) for the next
report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 14:44:05 +02:00
enricobuehlerandClaude Fable 5 fb8deb31a5 feat(client): HDR pass-through on the D3D11VA path
A PQ stream on the D3D11VA backend was always tone-mapped to sRGB by the
VideoProcessor — with D3D11VA now auto's first choice on Intel (40030e90),
Intel Windows users would have lost HDR entirely. When the presenter can
import an RGB10A2 D3D11 texture AND offers an HDR10 swapchain (the new
VulkanDecodeDevice::d3d11_hdr10 probe), the hand-off ring switches to
RGB10A2 and the VideoProcessor does a pure colorspace conversion (YCbCr
G2084 -> RGB G2084, no tone mapping); the emitted frame carries PQ/BT.2020
color, so the presenter flips its HDR10 swapchain and video image exactly
as it does for Vulkan Video PQ frames, and the blit passes the PQ values
through untouched. SDR-only paths keep the tonemap-to-sRGB BGRA8 ring;
in-band PQ flips rebuild the ring like a resize (generation-bumped).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 14:19:35 +02:00
enricobuehlerandClaude Fable 5 40030e90c8 fix(client): vendor-aware Windows decode order — D3D11VA first on Intel
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 24s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 53s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 6m9s
ci / bench (push) Successful in 6m51s
apple / screenshots (push) Successful in 6m14s
flatpak / build-publish (push) Successful in 6m57s
deb / build-publish-host (push) Successful in 10m19s
docker / deploy-docs (push) Successful in 28s
deb / build-publish (push) Successful in 11m30s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 6m38s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 8m56s
arch / build-publish (push) Successful in 15m27s
android / android (push) Successful in 16m10s
ci / web (push) Successful in 58s
ci / rust (push) Successful in 20m46s
ci / docs-site (push) Successful in 1m2s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 8m0s
apple / swift (push) Successful in 1m19s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m15s
decky / build-publish (push) Successful in 19s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m58s
Intel's Windows driver advertises Vulkan Video (Arc drivers since 2023), so
the capability gate alone no longer keeps Intel off FFmpeg-Vulkan — and that
combination is broken in the field (B580 report: strobing + 7.2 ms p50
decodes at 4K120) and on glass (Arc Pro iGPU: 29-33 ms p50 decodes at 4K60
where D3D11VA does 2.5 ms on the SAME GPU). Mirror the Linux vendor order on
Windows: NVIDIA/AMD keep Vulkan Video first, Intel/unknown take D3D11VA
first; vulkan stays reachable by explicit preference and as auto's fallback.

Also:
- Vulkan→D3D11VA mid-session demotion rung (the Windows analog of Linux's
  Vulkan→VAAPI rung) — a failing Vulkan backend lands on hardware, not on
  software, which cannot survive 4K120.
- The demotion streak now needs 1 s of age as well as 3 consecutive errors:
  a startup loss burst produced 3 errors in 20 ms and stranded the session
  on software decode (one-way) before the IDR requested on the first error
  could possibly arrive — live-hit on the Intel iGPU leg.
- Stale "Intel's Windows driver has no Vulkan Video" comments corrected,
  docs updated to the per-vendor order, vendor-order test extended with
  discrete/iGPU Arc cases.

Verified on an Arc Pro iGPU + RTX 3500 Ada laptop against a CachyOS/NVENC
host: auto now picks d3d11va on Intel (60 fps, 2.5-2.8 ms decode, e2e 26 ms
vs 50 ms before) and still picks vulkan on NVIDIA. 27/27 pf-client-core
tests, clippy clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 14:04:32 +02:00
enricobuehlerandClaude Fable 5 188edde2b3 feat(pyrowave): Windows host HDR + 4:4:4, Rust client HDR present
Phase 3 of design/pyrowave-444-hdr.md. A PyroWave session now negotiates HDR
(10-bit) and 4:4:4 on a Windows host exactly like HEVC/AV1, and the Linux
client presents it through the real HDR10 path.

Host (Windows): BgraToYuvPlanes becomes mode-aware — SDR/BGRA and HDR/scRGB
variants at half- or full-res chroma. The HDR passes reuse HdrP010Converter's
exact colour math (scRGB -> PQ BT.2020 limited studio codes, verified by
hdr_p010_selftest) but write P010-style MSB-packed codes into two separate
shareable R16_UNORM/R16G16_UNORM textures; chroma keeps the pyrowave family's
centre-sited 2x2 box. idd_push pins the composition to the NEGOTIATED depth
(SDR sessions force advanced color off as before; 10-bit sessions enable it
and ride the FP16 ring), and the descriptor poller re-asserts that state
instead of following display flips the fixed-format encoder can't. The
encoder imports 8/16-bit planes per session and stamps the sequence header's
BT.2020/PQ/matrix bits on HDR (stamp_color_bits, extending 574e3e4e's range
stamp); supports_10bit/can_encode_10bit/can_encode_444 gates open (HDR
Windows-only — Linux capture has no HDR source).

Client: the plane ring becomes R16_UNORM for 10-bit sessions (with a
STORAGE_IMAGE format probe), the planar CSC pass joins the HDR10 swapchain
rebuild (set_hdr_mode previously destroyed it without rebuilding — latent),
st.hdr follows frame.color.is_pq(), and the planar push constants carry
depth-10 MSB-packed rows + the PQ tonemap mode, identical to the NV12 arm.

Verified: .173 (RTX 4090) deploy-config clippy + fmt + wire tests + the
extended pyrowave_win_smoke (10-case {SDR,HDR}x{420,444} matrix incl. R16
imports and header stamps); .21 (RTX 5070 Ti) clippy across 4 crates, host
186 tests, client/presenter/encode tests, both Linux GPU smokes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:21:06 +02:00
enricobuehlerandClaude Fable 5 5eb930e71d feat(pyrowave): negotiation plumbing for 4:4:4 + HDR — thread chroma/depth/ColorInfo end to end
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 6m49s
ci / web (push) Successful in 58s
ci / docs-site (push) Successful in 1m10s
arch / build-publish (push) Successful in 11m34s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 6m43s
ci / bench (push) Successful in 5m21s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 8m9s
deb / build-publish-host (push) Successful in 9m20s
android / android (push) Successful in 22m17s
flatpak / build-publish (push) Successful in 6m12s
ci / rust (push) Successful in 19m3s
deb / build-publish (push) Successful in 12m40s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 9m8s
apple / swift (push) Successful in 1m18s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m17s
docker / deploy-docs (push) Successful in 11s
apple / screenshots (push) Successful in 6m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 23m5s
windows-host / package (push) Successful in 16m27s
Phase 1 of design/pyrowave-444-hdr.md. No behavior change yet: the handshake's
4:4:4 gate now admits PyroWave (probe = can_encode_444(codec), capture gate
inherently satisfied — the wavelet path always ingests an RGB source and does
its own CSC), but can_encode_444 stays false for PyroWave until the per-OS
full-res-chroma CSC variants land (Phase 2 Linux, Phase 3 Windows), so every
session still resolves 4:2:0/8-bit.

- Both host encoders take the negotiated ChromaFormat (bail on 444 for now);
  the PUNKTFUNK_ENCODER=pyrowave lab override pins 4:2:0.
- Bitrate: the automatic ~1.6 bpp pin resolves AFTER depth+chroma and scales
  x1.625 for 4:4:4 / x1.15 for 10-bit (factors from the Phase-0 fixture
  matrix); the mid-stream mode-switch re-resolve threads the session's values.
- Client: PyroWaveDecoder builds its plane ring (full-res chroma when 444) and
  creates the upstream decoder from the negotiated chroma, keeps chroma fixed
  across mid-stream resizes, drops the even-dims requirement for 444, and
  returns the negotiated Welcome ColorInfo as the frame colour contract
  instead of hardcoded BT.709 (the wavelet bitstream has no VUI).

Verified on .21 (RTX 5070 Ti): clippy -D warnings (host+client+encode), host
186 tests, client + pf-encode tests, fmt, and the pyrowave_smoke GPU
round-trip through the patched vendored lib (97cf15e3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:37:12 +02:00
enricobuehlerandClaude Fable 5 aedee2a4e3 perf(latency): tier-0 attribution + tier-1 send-path levers from the latency plan
ci / web (push) Successful in 49s
ci / docs-site (push) Successful in 54s
decky / build-publish (push) Successful in 18s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
apple / swift (push) Successful in 1m23s
ci / rust (push) Failing after 4m15s
arch / build-publish (push) Failing after 4m35s
deb / build-publish (push) Failing after 4m4s
docker / deploy-docs (push) Successful in 24s
ci / bench (push) Successful in 5m35s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 3m45s
windows-host / package (push) Failing after 9m8s
release / apple (push) Successful in 5m49s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 5m25s
flatpak / build-publish (push) Failing after 8m3s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m58s
android / android (push) Successful in 14m9s
apple / screenshots (push) Successful in 6m28s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 7m0s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 9m28s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 12m26s
design/latency-reduction-2026-07.md T0.1/T0.2/T1.2/T1.3:

- T1.2 rate-capped front-loaded pacing: the paced overflow's budget is now
  min(0.9x slack, overflow wire time at ~3x the live encoder bitrate)
  (PUNKTFUNK_PACE_FACTOR, 0 = legacy deadline-only spread). A 300 KB-1 MB
  frame's tail leaves in ~2-5 ms instead of smearing across ~15 ms at 60 fps;
  GameStream schedule byte-identical (pins unchanged).
- T1.3 data-first wire order: packetize emits every block's data shards before
  any parity (per-block parity pools keep all blocks' parity alive for the
  second pass), so lossless completion stops waiting behind the parity tail.
  EOF = last emitted packet; receiver already order-agnostic.
- T0.1 staged 0xCF: HostTiming gains an append-extensible per-stage tail
  (queue/encode/pace us; seal+channel-wait derived as residual) - no cap bit
  needed, old peers read the 13-byte prefix. Joined client-side into
  Stats::host_{queue,encode,xfer,pace}_ms, the OSD detailed tier, and the
  probe's report.
- T0.2 true on-glass present timing: VK_KHR_present_id/present_wait enabled
  when supported; a PresentTimer waiter thread resolves each present id to
  real visibility, replacing the submit-time display stamp (which undercounts
  by up to a refresh and hides a silent-FIFO standing queue).

Validated on .21: core 185 + host 185 tests, pf-presenter 19, clippy
-D warnings across all five touched crates; loss-harness recovery curve
unchanged; C ABI harness round-trips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 19:13:08 +02:00
enricobuehlerandClaude Fable 5 871ebb31ce feat(clients): render-scale setting on every client — shared punktfunk_core::render_scale
Client-side supersampling/downscaling: the client asks the host to render
and encode at chosen-resolution × scale (the host does no scaling) and the
presenter rescales the decoded frame to the display. >1 supersamples for
sharpness; <1 lightens the host GPU and the link. Default 1.0 = Native, the
prior behavior.

The geometry lives once in punktfunk_core::render_scale (multiply, preserve
aspect ratio, floor to even, clamp to the codec's per-axis ceiling — 4096
for H.264, 8192 otherwise), the Rust twin of the Apple client's
RenderScale.swift, consumed by the native session client, the presenter's
match-window path, the Windows/Linux settings UIs, Decky, and Android
(settings + host connect + unit test).

Implemented and platform-verified by the Apple-client-features session
(Linux+Android+Apple green there); the punktfunk-core wiring
(pub mod render_scale) is restored here after being lost in a working-tree
reconciliation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:20:15 +02:00
enricobuehlerandClaude Fable 5 13b1f36d4a feat(core,clients): one rumble policy engine for every platform (rumble root fix D)
punktfunk-core client/rumble.rs: a per-connection policy engine consumes seq-gated wire
updates and emits EFFECTIVE actuator commands — re-emits on renewals (duration APIs stay
re-armed), self-silences at the v2 lease, a UNIFORM 1 s legacy-host staleness replacing the
per-platform zoo (Apple 1.6 s / Android 60 s / SDL 1.5 s / Deck 1 s), quirk-declared
actuator keepalives (Deck 40 ms + LSB dedupe-defeat jitter), and one stop per buzzing pad
on connection close. Per-pad mailbox semantics: a stalled embedder wakes to ONE current
command, and a stop can structurally never be the update an overflowing queue drops.

New API/ABI: NativeClient::{next_rumble_command,set_rumble_quirks} +
punktfunk_connection_next_rumble_cmd/_set_rumble_quirks (next_rumble/next_rumble2 stay for
un-migrated embedders; both consumers are fed). Migrations DELETE the platform forks:
pf-client-core loses RumbleState + the Deck keepalive loop + LEGACY_RUMBLE_CEILING_MS and
physically silences a slot at close; Android loses the 60 s legacy one-shot (backstop
repack, cancel-on-zero); Apple loses envelopeDeadline + sessionStaleSeconds + both tick
watchdogs (CoreHaptics realization untouched; mac xcframework rebuilt locally).

design/rumble-root-fix.md par. D. Engine 10/10 unit tests; core tests 176 Linux / 175
Windows + clippy -D warnings; swift build + RumbleTuningTests; Kotlin + android-native
compile green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 14:07:32 +02:00
enricobuehlerandClaude Opus 4.8 570ff504ad refactor(client-core/W8): split video.rs into flat decoder-backend siblings
Break the 1974-line pf-client-core/src/video.rs into flat sibling modules
(matching the crate's video_d3d11.rs / video_pyrowave.rs convention), leaving
video.rs as the contract + Decoder dispatch facade:
  - video_color.rs   : ColorDesc + csc_rows (the Y'CbCr->RGB matrix)
  - video_software.rs : the libavcodec/swscale SoftwareDecoder
  - video_vaapi.rs   : the Linux-only VAAPI/DRM-PRIME backend (mod is cfg(linux))
  - video_vulkan.rs  : the FFmpeg Vulkan Video backend
Every crate::video::X / video::X path stays byte-stable (ColorDesc + csc_rows
re-exported from video.rs; frame POD, VulkanDecodeDevice, QueueLock, Decoder,
decodable_codecs*, ffmpeg_codec_id, fourcc/drm_fourcc_for all stay in video.rs).
Code-driven placements: averr, AVERROR_EAGAIN, frame_is_keyframe stay in video.rs
(shared by all three decoders); DrmFrameGuard's field + drm_fourcc_for +
Software/Vaapi/VulkanDecoder ctors/decode became pub(crate) (sibling access);
the test module split three ways (software tests need private decoder internals).
Pure move; no behavior change.

Verified on Linux (home-worker-5): cargo clippy -p pf-client-core (default
[pyrowave] + --no-default-features, --all-targets -D warnings) + cargo test.
Windows verify BLOCKED environmentally: pf-client-core -> sdl3 build-from-source
-> CMake/CL.exe fails on winbox's non-ASCII home path (fails the baseline too,
independent of this split); the split's Windows surface (facade cfg(windows) bits
+ video_d3d11) is verbatim-preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:06:57 +02:00
enricobuehlerandClaude Opus 4.8 571e22bc0f refactor(core): consolidate the fingerprint-pinning verifier into core::tls
Per plan §2.5: the security-critical rustls fingerprint-pinning ServerCertVerifier
was hand-rolled three times — quic/endpoint.rs (PinVerify), pf-client-core
library.rs, punktfunk-tray status.rs — drifting copies on a trust boundary. Add
one canonical punktfunk_core::tls::PinVerify (+ cert_fingerprint) behind a light
`tls` feature (rustls + sha2 only, no QUIC runtime); `quic` now depends on it, and
quic::endpoint re-exports cert_fingerprint so that path stays byte-stable
(gamestream + pf-client-core reach it there).

- core::tls::PinVerify: new(pin) for the HTTP clients, with_observed(pin, slot)
  for the QUIC TOFU connect. Behavior-identical to all three originals (pin-check
  + real CertificateVerify signature verification; only hashes the leaf when a pin
  or observed slot needs it). Two focused unit tests anchor the boundary.
- quic/endpoint.rs: drop the private PinVerify, wire client_pinned through
  tls::PinVerify::with_observed.
- pf-client-core library.rs + tray status.rs: use the shared verifier; tray also
  routes load_pin through core cert_fingerprint and drops its direct sha2 dep,
  gaining only the light core `tls` feature (still no host dep, no QUIC runtime).

Verified on Linux (home-worker-5): clippy 0/0 for core(quic), core(tls),
pf-client-core, tray, host(nvenc,vulkan-encode,pyrowave); core 153 lib tests +
loopback 7/7 (pinned handshake) + c_abi round-trip green.

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 12:42:53 +02:00
enricobuehlerandClaude Fable 5 01266ff18d feat(gamepad): SC2 Puck-dongle passthrough with the native 28DE:1304 topology
Community-contributed round 5 of the Steam Controller 2 passthrough,
reviewed + verified. A Puck-captured pad now presents the dongle's real
seven-interface identity (CDC pair, four controller HID slots, management
HID) instead of relabelling its reports as a wired 1302 — Steam's Puck
feature dances (wireless_transport / esb/bond / 0xB4 slot status) get
capture-shaped answers, and the wired identity's canned replies are
corrected to the real captures (attribute count, string-attr framing,
0xF2 firmware info, bcdDevice nibble encoding).

- new wire pref 10 = SteamController2Puck (Hello/Welcome byte; older
  peers degrade to Auto), selected by the Android capture link when the
  transport is a dongle, or by VID/PID in the degraded InputDevice path
- TRITON_RDESC is now the captured numbered descriptor (mouse/keyboard
  lizard collections + per-id vendor reports); unnumbered framing made
  hidraw mangle feature report 2 and Steam eventually closed the device
- interrupt-IN now queues sparse reports (battery/RSSI/wireless edges)
  instead of keeping latest-only, so a 250 Hz state packet can no longer
  erase them before the USB/IP poll observes them; EP0 SET_REPORT is
  split by wValue report type (OUTPUT parsed for rumble vs FEATURE)
- vendored usbip-sim: config attributes/max-power, IAD prefix + BOS
  descriptor support, correct BCD minor.patch encoding (Deck's 0x0300/
  0x0200 values are nibble-zero, so its bytes are unchanged), and
  full-speed interrupt pacing in ms (was 8 kHz from the HS formula)
- Triton feedback is serviced at 1 kHz while an SC2 backend exists so
  Steam's trackpad haptic writes reach the client unbatched

Verified: clippy -D warnings + 319 host tests green on Linux, core wire
tests green, Android kit/app compile + unit tests green. On-glass Puck
retest owed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 20:47:52 +02:00
enricobuehlerandClaude Opus 4.8 ff38933312 feat(core,apple,session): report decode latency from the Apple + Windows/Linux clients too
ci / web (push) Successful in 51s
ci / docs-site (push) Successful in 1m4s
ci / rust (push) Failing after 6m46s
decky / build-publish (push) Successful in 20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
ci / bench (push) Successful in 5m19s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
arch / build-publish (push) Successful in 11m12s
windows-host / package (push) Successful in 14m52s
flatpak / build-publish (push) Successful in 7m10s
android / android (push) Successful in 16m0s
docker / deploy-docs (push) Failing after 17s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m8s
deb / build-publish (push) Successful in 14m3s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m41s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m8s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m18s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m54s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m31s
apple / swift (push) Successful in 5m19s
release / apple (push) Successful in 27m46s
apple / screenshots (push) Successful in 19m39s
Extends 56f9c8c4 (the Automatic-bitrate decode signal, core + Android) to the remaining
clients, so every platform caps Automatic at its real decoder limit instead of the network
link ceiling — the fix for a fast LAN feeding a slower hardware decoder.

- core/abi: punktfunk_connection_report_decode_us + _wants_decode_latency expose the
  NativeClient methods to the C-ABI embedders (regenerated punktfunk_core.h, additive only).
- apple: PunktfunkConnection wrappers + Stage2Pipeline reports received→decoded from the
  VideoToolbox decode-completion callback — every decoded frame, before the newest-wins ring
  can drop the backlog. Stage-1 (AVSampleBufferDisplayLayer, no per-frame decode callback)
  stays network-only; stage-2 is the metered path.
- windows/linux: the shared punktfunk-session client (pf-client-core) links core directly, so
  it calls the NativeClient methods — report received→decoded from the pump, gated on
  wants_decode_latency. Exact for the synchronous D3D11VA/software decode; received→submit
  (still the decoder-input backpressure signal) for the async Vulkan-Video path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 17:43:54 +02:00
enricobuehlerandClaude Opus 4.8 9127c3465f feat(client,host): PyroWave Apple Metal decoder + per-mode bitrate pin
- clients/apple: native Metal wavelet decoder + compute shaders (Phase 5),
  decoding PyroWave without embedding MoltenVK.
- pf-client-core: plumb user_flags/completeness through Decoder::decode_frame
  so the PyroWave backend parses chunk-aligned + partial AUs; gate the param's
  unused-warning to exactly the non-pyrowave builds (fixes -D warnings on the
  featureless Linux client build).
- punktfunk-host: on a mid-stream mode switch, re-resolve the "Automatic"
  PyroWave bitrate for the new mode's ~1.6 bpp operating point (explicit rates
  and H.26x ABR stay put); reject sub-128px PyroWave modes before the encoder
  rebuild instead of after the ack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:47:42 +02:00
enricobuehlerandClaude Fable 5 2621b6e6b1 feat(core,host,android): Steam Controller 2 as-is passthrough to Linux hosts
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 1m2s
ci / rust (push) Failing after 5m34s
decky / build-publish (push) Successful in 17s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 52s
ci / bench (push) Successful in 6m3s
docker / deploy-docs (push) Successful in 25s
android / android (push) Successful in 14m2s
arch / build-publish (push) Successful in 11m57s
deb / build-publish (push) Successful in 11m49s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m5s
flatpak / build-publish (push) Failing after 8m3s
windows-host / package (push) Failing after 8m32s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m6s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m18s
apple / swift (push) Successful in 5m4s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 5m14s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 5m43s
release / apple (push) Successful in 26m11s
apple / screenshots (push) Has been cancelled
The 2026 Steam Controller (Valve "Ibex" / SDL "Triton") captured on an
Android client is passed through AS-IS: the host presents a virtual pad
with the real wired identity (28DE:1302) and mirrors the physical pad's
raw HID reports, so Steam on the host drives it over hidraw exactly like
the real thing — trackpads, gyro, paddles, and its rumble/settings writes
flow back onto the physical controller. Protocol ground truth: SDL's
Valve-maintained SDL_hidapi_steam_triton.c + steam/controller_structs.h.

Core:
- GamepadPref::SteamController2 (wire byte 9; names steamcontroller2/
  sc2/ibex) + PUNKTFUNK_GAMEPAD_STEAMCONTROLLER2 in the C ABI.
- Raw HID planes: RichInput::HidReport (0xCC/0x04, client→host input
  reports verbatim, Copy fixed-64 body) and HidOutput::HidRaw (0xCD/0x05,
  host→client feature/output writes for replay). Best-effort is sound by
  the device protocol's own design (rumble re-sent every ~40 ms, settings
  every ~3 s — losses self-heal); HidRaw bypasses hidout dedup for
  exactly that reason.

Host (Linux):
- triton_proto.rs + steam_controller2.rs: Triton2Manager UHID backend —
  no kernel driver binds the PID (hidraw only; Steam Input is the
  consumer), raw mirroring with a typed-fallback 0x42 synthesizer until
  the first raw report, SET_REPORT ack + raw forward, canned GET_REPORT
  serial reply, rumble also parsed onto the universal 0xCA plane (phone
  mirror). Rides the uhid + 28DE-conflict degrades; UHID promotion by
  Steam is flagged in the creation log (usbip transport is the known
  follow-up if Steam ignores Interface:-1 devices for Triton too).

Android:
- Sc2UsbLink (wired/Puck: vendor-interface claim detaches the OS driver,
  interrupt read loop, lizard-off on the watchdog cadence, raw replay via
  interrupt-OUT / SET_REPORT with hidapi report-id framing) and Sc2BleLink
  (Valve vendor GATT service, notify subscribe machine, 0x45 re-framing,
  HIGH connection priority).
- Sc2Capture orchestrator: raw plane + typed mirror (exit chord + host
  degrade paths keep working) on a GamepadRouter external slot; raw
  return path via GamepadFeedback.onHidRaw.
- nativeSendPadHidReport JNI (direct ByteBuffer, no per-report copy),
  hidout raw decode, usb-host/BLUETOOTH_CONNECT manifest bits, opt-out
  settings toggle, StreamScreen engagement incl. the USB permission flow.

Verified: core 149 + host 312 tests green on Linux (.21), on-box uhid
smoke creates/mirrors/tears down the virtual 28DE:1302, C ABI harness
round-trips, Android compileDebugKotlin green. On-glass with the real
controller owed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 11:22:16 +02:00
enricobuehlerandClaude Fable 5 705a8baddf feat(core,host,client): PyroWave datagram-aligned packets + partial-frame delivery (Phase 4, §4.4)
ci / web (push) Successful in 1m10s
ci / docs-site (push) Successful in 1m16s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
decky / build-publish (push) Successful in 27s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
apple / swift (push) Successful in 4m59s
ci / rust (push) Failing after 6m17s
ci / bench (push) Successful in 6m36s
docker / deploy-docs (push) Successful in 23s
flatpak / build-publish (push) Failing after 8m29s
arch / build-publish (push) Successful in 11m11s
android / android (push) Successful in 12m33s
deb / build-publish (push) Successful in 14m8s
windows-host / package (push) Successful in 14m56s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m2s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m4s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m29s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m6s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 5m15s
release / apple (push) Successful in 26m8s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 5m48s
apple / screenshots (push) Successful in 20m29s
PyroWave AUs now packetize on the negotiated shard payload, so a lost datagram
costs a few wavelet blocks of localized blur rather than a whole frame — and the
client can render an aged-out lossy frame instead of freezing until the next one.

Host (opt-in, PyroWave only):
- The encoder packetizes at the shard payload behind a 4-byte window prefix
  (used-len u16 + kind u16). Whole packets pack into WIN_PACKED windows; a packet
  too large for one shard (PyroWave 32x32 blocks are atomic and can exceed a
  shard) rides a WIN_FRAG_FIRST/CONT/LAST chain. `set_wire_chunking()` joins the
  Encoder trait (forwarded through TrackedEncoder — the silent-no-op trap);
  EncodedFrame.chunk_aligned marks the AU.
- virtual_stream tags the AU with USER_FLAG_CHUNK_ALIGNED and re-applies chunking
  after every encoder (re)build, the adaptive-bitrate rebuild included.

Core:
- USER_FLAG_CHUNK_ALIGNED (0x40) wire bit. Reassembler opt-in
  (set_deliver_partial): a chunk-aligned frame that ages out with holes is handed
  over as Frame{complete:false} — received shards at their exact offsets, missing
  ranges zero-filled — instead of being dropped. Partials age out on a tight 30ms
  fuse (PARTIAL_WINDOW_NS) instead of the 120ms loss window: each frame is
  independently decodable, so an ancient partial has no value in a live stream.
  Newest-wins. A partial still counts as dropped for loss reporting.

Client (PyroWave decode):
- The session opts in when codec == PyroWave. The decoder walks the AU
  window-by-window, skipping zero (missing) windows and reassembling FRAG chains,
  then decodes whatever survived. A newest-decoded-index guard drops partials the
  pump has already moved past (no time-travel present).

Also fixes a redundant-closure clippy nit in the PyroWave planar-present path.

Validated on an RTX 5070 Ti under 2% netem loss with FEC pinned off: 60fps
sustained entirely via partials, e2e 43ms p50 (146ms before the fuse) vs 23ms
lossless, no keyframe-recovery chatter. Tests green: core 149, host 310 + the
GPU-gated encoder smoke (framed-window walk + FRAG reassembly + upstream
round-trip), client 26; clippy clean on the pyrowave feature combos.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 11:14:24 +02:00