4803260affb8eddea56483c736c6f3399e26c80b
2051
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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). |
||
|
|
8a5a5edc37 |
fix(small crates): the proof lint now covers every first-party crate but one
Six crates were still unguarded, and all six are OURS — none vendored: pf-console-ui, pf-gpu, punktfunk-tray, clients/windows, tools/display-disturb, wdk-probe. Two of them ship (the tray and the Windows client), so "small" was about item count, not exposure. Five are closed here. Only 17 of their 75 unsafe items actually lacked a proof — pf-gpu, punktfunk-tray and display-disturb were already fully documented and needed nothing but the deny, which is the good case: the convention was being followed, just not enforced. The 17 that were missing are the usual Win32/COM shapes, and two were worth stating properly. `clients/windows`'s `GetCurrentPackageFullName` is called with `len = 0` and no buffer — that is the documented identity PROBE, which writes nothing, and reading it as a normal query would be a mistake. `pf-console-ui`'s two `destroy_image_view` calls are the load-bearing ones: the comment above one already argued that in-flight sampling of that slot ended two presents ago (the ring alternates and the presenter waits its fence before each record), which is exactly the kind of reasoning a `// SAFETY:` should carry and it was sitting there unlabelled. Also fixes a real Windows-only clippy error this uncovered: `pf-gpu` had a `#[cfg(target_os = "windows")]` fn AFTER its `mod tests`, tripping `items_after_test_module`. It never fired on Linux (the item does not exist there) and no CI job clippies pf-gpu on Windows, so it sat unseen. Moved above the test module. Remaining: `wdk-probe` (26 items) alone, and only because it needs the WDK to build — .47 cannot, so nothing here can verify a deny on it. Verified: Linux .21 fmt + both CI clippy steps rc=0; Windows .47 the four Windows-relevant crates at `-D warnings` rc=0. |
||
|
|
65cd388a52 |
fix(presenter,core): close the last of the proof-lint hole — the Vulkan contract, stated once
pf-presenter's 120 sites are `ash` calls almost without exception, so 120 independent arguments
would have been 120 restatements of the signature — the exact noise this program exists to remove.
They get the `abi.rs` treatment instead: the Vulkan contract stated once in `lib.rs`, each site
naming which of three shapes it is.
The three are not equal, and separating them is the point. CREATE and RECORD carry no real
precondition — the device is owned, the builders are locals, nothing executes until submit. DESTROY
does: the GPU must not still be using the object, and that is established by the path (a fence wait,
a `queue_wait_idle`, a retired swapchain), not by the call. Those sites say so, because getting it
wrong is a use-after-free no type catches. The contract also tells the next person that a block
outside the three shapes needs a real proof, and that writing "as above" is the signal it doesn't
belong in them.
punktfunk-core's Windows half is finished here too: `qos_windows.rs`'s `GetLastError` reads (called
before anything can reset the thread's error slot) and `udp/windows.rs`'s control-message write,
whose argument is that `ctrl` is sized by `WSA_CMSG_SPACE(4)` — computed two lines up — so header
plus payload cannot run past it, and `write_unaligned` is used because `WSA_CMSG_DATA` offers no
alignment guarantee.
⚠️ THE WINDOWS BLIND SPOT BIT A THIRD TIME. A Linux measurement put this crate pair at 113; the
real number was 129 — `d3d11.rs`, `win32.rs`, `qos_windows.rs`, `udp/windows.rs` are all
`cfg`-hidden. Every crate in this sweep had to be finished on .47 after being "done" on .21. For a
cross-platform crate the Linux number is a lower bound, never the answer.
All three crates now deny `undocumented_unsafe_blocks`, which was the goal: it applied to 8 of 11
crates carrying unsafe, and the three exempt ones held 381 items between them — including the C ABI
surface and the presenter. Verified: Linux .21 fmt + both CI clippy steps rc=0; Windows .47 all four
closed crates clippy `-D warnings` rc=0 plus the full Windows CI clippy set and pf-capture's tests.
Only small crates remain unguarded (75 items total, largest 26).
|
||
|
|
dda9ed1aa2 |
fix(core): close the proof-lint hole in punktfunk-core — the ABI contract, stated once
146 sites, 141 of them in `abi.rs`, the `extern "C"` surface `cbindgen` turns into `punktfunk_core.h`. Writing 141 independent arguments would have been the wrong answer and would have read like it: they are instances of ONE contract in six shapes — opaque handles reached only through `as_mut()`/`as_ref()` (so null becomes a status, never a dereference), caller-owned out-params (null-checked exactly where the header calls them optional), NUL-terminated-or-null C strings through `opt_cstr`, unchanged forwarding to a versioned entry point, fixed-length output buffers, and `addr_of!` reads. So the contract is stated once at the top of the file, the way `pf-win-display`'s CCD contract is, and each site says which instance it is. Two properties that hold everywhere are stated there and not repeated 141 times: no pointer is retained past the call that received it, and every entry point runs inside `guard`'s `catch_unwind`, so a panic becomes a status code instead of unwinding into C. The `addr_of!` sites got a real proof rather than a shape, because that one is load-bearing and non-obvious: it forms a raw pointer WITHOUT creating a reference precisely because the caller's struct may be an older, smaller version, so the field must be read by offset and not through a `&`. `transport/udp/linux.rs`'s five are genuinely individual — two `zeroed()` POD initialisations, and the `sendmmsg`/`recvmmsg`/`CMSG` trio, where the argument worth writing down is that `msg_controllen` is set to `CMSG_SPACE(size_of::<u16>())` and the `CmsgBuf` is 64 bytes, so the kernel cannot write past the control buffer. punktfunk-core now denies `undocumented_unsafe_blocks`. Verified: Linux .21 fmt + both CI clippy steps rc=0, and — since this is the C surface and comments alone should not change it — the C ABI harness still passes, 4 frames round-tripped byte-exact through lossy loopback. |
||
|
|
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.
|
||
|
|
192635545d |
fix(cli): the lockfile never learned about the new crate
audit / bun-audit (plugin-kit) (push) Successful in 55s
audit / bun-audit (sdk) (push) Successful in 54s
audit / bun-audit (web) (push) Successful in 54s
audit / docs-site-audit (push) Successful in 54s
audit / pnpm-audit (push) Successful in 23s
audit / cargo-audit (push) Successful in 2m30s
ci / docs-site (push) Successful in 1m2s
ci / web (push) Successful in 3m12s
apple / swift (push) Successful in 5m16s
android / android (push) Failing after 6m58s
ci / bench (push) Successful in 5m36s
audit / license-gate (push) Successful in 8m53s
ci / rust-arm64 (push) Failing after 8m52s
ci / rust (push) Failing after 8m53s
deb / build-publish (push) Failing after 5m16s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 23s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 22s
decky / build-publish (push) Successful in 29s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 14s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 12s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 1m2s
docker / build-push-arm64cross (push) Successful in 18s
docker / deploy-docs (push) Successful in 1m0s
windows-host / package (push) Successful in 12m44s
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) Failing after 1m47s
arch / build-publish (push) Successful in 15m10s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Failing after 1m50s
deb / build-publish-client-arm64 (push) Successful in 7m37s
deb / build-publish-host (push) Successful in 10m24s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m21s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m1s
flatpak / build-publish (push) Failing after 9m31s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 13m59s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 13m55s
release / apple (push) Successful in 29m42s
apple / screenshots (push) Successful in 22m31s
Adding `clients/cli` as a workspace member without regenerating `Cargo.lock` breaks every packaging script — deb, rpm, arch, nix and flatpak all build with `--locked`, which refuses to resolve a member the lock doesn't know. It slipped through because the CLI was only ever built on the remote boxes, and their lock updates stayed on those boxes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bf98102714 |
feat(packaging): ship punktfunk everywhere, and deprecate the session's stray pairing flag
ci / web (push) Successful in 57s
ci / docs-site (push) Successful in 1m5s
android / android (push) Canceled after 3m17s
apple / swift (push) Canceled after 3m18s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 3m18s
ci / rust (push) Canceled after 3m19s
ci / rust-arm64 (push) Canceled after 3m20s
ci / bench (push) Canceled after 3m18s
deb / build-publish (push) Canceled after 2m23s
deb / build-publish-host (push) Canceled after 2m13s
deb / build-publish-client-arm64 (push) Canceled after 0s
decky / 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
flatpak / build-publish (push) Canceled after 2s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
windows-host / package (push) Canceled after 3m25s
windows-host / winget-source (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 / build (aarch64-pc-windows-msvc) (push) Canceled after 0s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
The rest of C1. A CLI that only exists in a dev tree is not a door anyone can use, so it now builds and installs in deb, rpm, arch, nix, flatpak and MSIX alongside the client and the session. On Windows it gets its own `AppListEntry="none"` application with the alias `punktfunk.exe` — a command, not a Start-menu tile — which is what the Playnite importer will shell to for `punktfunk library <host> --json`. `punktfunk-session --pair` prints a deprecation notice and forwards. Pairing is a trust ceremony and belongs to the brain, fronted by `punktfunk pair` or a shell; a renderer owning one is precisely the mixing of concerns this split exists to undo. It keeps working for a release, because someone's provisioning script is calling it today. Decky's invocations are untouched and verified still working (`--list-hosts` JSON shape unchanged, `--reachable` exit codes unchanged): those flags are a frozen compat contract until Decky migrates to the verbs at its own pace. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
0594056e02 |
feat(cli): punktfunk — one headless front-end over the brain
C1 of design/client-architecture-split.md. There were four overlapping, none-complete CLIs:
the Linux shell's rich headless verbs, the Windows shell's near-none (it could not start a
stream at all), the session's own, and punktfunk-probe's diagnostics. This is the one a
script or a plugin should reach for, and it is a FRONT-END, not the brain — policy stays in
`pf-client-core` and the GUI shells keep calling it in-process. Shelling out for connects
would have traded duplicated code for a duplicated IPC protocol.
punktfunk pair | hosts list/add/forget | wake | library | launch | open
reachable | speed-test | profiles list | reset
Because it runs the same plan builder and the same wake machine as a card click, `launch`
and `open` WAKE A SLEEPING HOST — which the Linux shell's exec-style `--connect` never did;
it fired a packet at best and dialled into the void. Host references resolve through the
shared `resolve_host`, so `punktfunk launch desk` and `punktfunk://connect/desk` cannot
disagree about which box "desk" is.
Exit codes extend the session's contract so a consumer can branch without parsing prose:
0 ok, 2 connect, 3 trust, 4 renderer, 5 nothing matched what you named, 6 refused because
it needs a person. That last one is why `pair` and `reset` check for a terminal before
prompting — a CLI that blocks a CI job on a hidden question is a hang, not a UX.
Two deliberate non-features. `speed-test` measures and prints but does NOT apply: which
layer a bitrate belongs in is a decision the GUI makes with the user (global vs the bound
profile), and a CLI silently rewriting a profile is exactly the surprise that rule exists
to prevent. And `open` refuses an unknown host rather than pairing it — a URL may never
pair or trust on its own, at any surface.
`library` keeps TSV as its default output because that is what Decky's consumer parses;
`--json` is the door for tools, and the Playnite importer shells to exactly that. The
existing shell flags are untouched: they are a frozen compat contract until Decky migrates.
Verified on .21 against hand-authored stores: hosts list (TSV and JSON, with each host's
resolved profile and pins), profiles list, a launch honouring the binding ("Game"), a
one-off overriding it ("Work"), the same through `open` with `profile=`, and the refusal
codes (6 for an unknown host, 5 for `punktfunk://pair/...`).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
7fb2ad266e |
ci: a newer push supersedes the queued run — concurrency groups everywhere
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-host (push) Canceled after 0s
deb / build-publish (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
audit / cargo-audit (push) Canceled after 0s
audit / bun-audit (plugin-kit) (push) Canceled after 0s
audit / bun-audit (sdk) (push) Canceled after 0s
audit / bun-audit (web) (push) Canceled after 0s
audit / docs-site-audit (push) Canceled after 0s
audit / pnpm-audit (push) Canceled after 0s
audit / license-gate (push) Canceled after 0s
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
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
release / apple (push) Canceled after 0s
flatpak / build-publish (push) Canceled after 0s
Every push-triggered workflow now declares
concurrency: { group: workflow+ref, cancel-in-progress: true }
A busy push cadence on main was stacking ~10 queued runs per commit —
the fleet was 110 runs behind tonight — while only the newest commit's
canary matters. Now each new push cancels the superseded queued/running
run of the same workflow. Release tags are unaffected: every tag is its
own ref, so tag runs never cancel each other. Gitea honors this for push
triggers (PR triggers don't cancel yet: gitea#35933).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
cd67bd5d30 |
ci(audit): scope decky to shipped deps; make docs-site non-blocking deterministically
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
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
audit / cargo-audit (push) Canceled after 0s
audit / bun-audit (plugin-kit) (push) Canceled after 0s
audit / bun-audit (sdk) (push) Canceled after 0s
audit / bun-audit (web) (push) Canceled after 0s
audit / docs-site-audit (push) Canceled after 0s
audit / pnpm-audit (push) Canceled after 0s
audit / license-gate (push) Canceled after 0s
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
First real-runner dispatch (run 13627) surfaced two calibration issues: * pnpm audit flagged 6 high advisories — all in decky's devDependencies (the rollup build toolchain), which never ship: the plugin bundle carries prod deps only. --prod scopes the gate to what users get, matching the locally-validated state (clean). * docs-site relied on job-level continue-on-error, which act_runner does not reliably honor — replaced with a step-level '||' warning so the known advisories can never take the run red while staying visible in the log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5d018eea7c |
ci: the supply chain accounts for itself — per-release SBOM, full-tree audits, a real license gate
CRA Annex I Part II groundwork (see punktfunk-planning design/cra-readiness.md, Phase 1): * sbom.yml + scripts/ci/gen-sbom.sh: every vX.Y.Z release gets a CycloneDX SBOM attached — syft over both Cargo.locks, all Bun/pnpm trees and the Swift Package.resolved (2,667 components), merged with compliance/sbom/manual-components.cdx.json for what no lockfile records (pyrowave/Granite/volk/Vulkan-Headers pins, libvpl, FFmpeg, SDL3, VB-CABLE, punktfunk-gamescope). * audit.yml: bun audit now covers sdk + plugin-kit (not just web), decky's pnpm tree is scanned, and docs-site runs non-blocking until its known CMS-chain advisories are cleared. All shipping trees verified green today. * license-gate: about.toml's allowlist claim is finally enforced — cargo-about 0.9.1 with --fail over BOTH workspaces. The old [crate.clarify] license-only syntax fails to deserialize under 0.9; migrated ring to a per-crate accepted extension and dropped the stale aws-lc-sys entry (workspace is ring-only). Both gates validated green locally. * drivers/Cargo.lock: sync the pf-dualsense→pf-gamepad rename — the crate rename updated the manifest but the Windows-only lockfile was never regenerated; cargo-about's metadata pass caught it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c27065c260 |
feat(client/windows): punktfunk:// opens the Windows client, and cards can write a shortcut
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
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
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
WP4 / D2. Protocol activation of a full-trust packaged app delivers the URI as the command line, so a browser prompt, `start punktfunk://…` and a written `.lnk` all arrive the same way — as a positional argument, the same door the Linux shell uses. What Windows does not give us is single-instancing: unlike GApplication, a second activation is simply a second process. So the first instance claims the named mutex `unom.punktfunk.client` and any later one hands its URL to the winner over `WM_COPYDATA` and exits — one window, and the link opens where the user's hosts already are. The hand-off retries while the primary's window is still coming up (a shortcut double-clicked during startup is the ordinary case), and a hand-off that finds nobody falls through so this process becomes the shell that opens it. A link is never silently dropped, which is also why the inbox is a queue: two shortcuts in quick succession are two links. Routing is the same four lines of translation as on Linux, because the decisions belong to `plan_from_link`: a resolved link becomes the call a tile click makes, with the same wake, trust and error surfaces. Never preempting a live session is checked here, since only this layer knows one is running. The manifest declares the protocol and an app execution alias rather than writing registry keys, so uninstall leaves nothing behind — and Windows' own "allow this app to handle punktfunk links?" prompt is the origin friction the design wants, not duplicated in-app. (Manifest comments are free of double hyphens: XML forbids them and makepri rejects the whole file, which is how the console flag broke the v0.15.0 build. A test parses the substituted manifest and asserts both declarations, so that can't rot silently.) "Create shortcut…" writes a `.lnk` targeting the ALIAS with the URL as an argument, not a `.url` internet shortcut: both work while the scheme is registered, only this one still works if it isn't, and targeting the alias keeps it valid across updates when the package path changes. Verified on the CI VM (192.168.1.133): clippy -D warnings clean and its tests green. Not yet exercised on glass — `start punktfunk://…` cold and with the app running, and a written shortcut, still want a real desktop session. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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. |
||
|
|
e2fd1c586b |
Merge pull request 'fix(vaapi): use FFmpeg bt2020nc matrix name' (#16) from sassycrown/fix-vaapi-bt2020nc into main
windows-host / package (push) Canceled after 0s
windows-host / winget-source (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
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
Reviewed-on: #16 |
||
|
|
22936bbc89 |
fix(vaapi): use FFmpeg bt2020nc matrix name
ci / rust (pull_request) Canceled after 0s
ci / rust-arm64 (pull_request) Canceled after 0s
ci / web (pull_request) Canceled after 0s
ci / docs-site (pull_request) Canceled after 0s
ci / bench (pull_request) Canceled after 0s
apple / swift (pull_request) Canceled after 0s
apple / screenshots (pull_request) Canceled after 0s
android / android (pull_request) Canceled after 0s
FFmpeg rejects bt2020 as an out_color_matrix value. Use its canonical bt2020nc name for non-constant-luminance BT.2020, matching the existing AVCOL_SPC_BT2020_NCL encoder VUI. Co-Authored-By: OpenAI Codex <noreply@openai.com> |
||
|
|
f5de661c9e |
feat(client/windows): overridden rows are marked and resettable, and the speed test targets the right layer
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
windows / build (aarch64-pc-windows-msvc) (push) Canceled after 0s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
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
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (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
The two remaining pieces of P1 on Windows, both closing the same kind of gap the Linux client closed earlier. Overridden rows in profile scope now say so — an accent-marked caption — and carry the only way back to inheriting. Without it a profile is a one-way door here too, because overrides are recorded on touch and never inferred from comparing values. Reset calls the shared `SettingsOverlay::clear`, so the list of what can be cleared stays in one place across both shells. It also bumps a revision counter: the catalog changes behind the controls and nothing the page reads as state does, and root state compares before re-rendering, so without the bump the row would keep showing the value it just dropped. The speed test writes to the layer the TESTED host reads bitrate from, not the global — measuring one host used to re-tune every other one. Resolved exactly the way a connect resolves it: the one-off the test was started with (a pinned tile carries one), else the host's binding, else the global; a dangling binding falls back. The button names its target, so it says where it will write before you press it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
60a85a1344 |
refactor(encode/windows): fourth fence off — ffmpeg_win.rs, and D3d11Hw::new joins VaapiHw
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
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
47 sites, 25 of them raw pointer dereferences: the same libav shape as `vaapi.rs`, so the same verdict — a proof here carries an argument rather than restating a call. `D3d11Hw::new` loses its marker for exactly the reason `VaapiHw::new` did, and the trio is now the clearest statement of the rule in the tree: `CudaHw::new` KEEPS it (handed a `CUcontext`), `VaapiHw::new` and `D3d11Hw::new` do not (a borrowed COM wrapper and scalars, opening what they need themselves). Three near-identical libav context builders, sorted by whether a caller can hand them something broken. Also unmarked, all for the same reason — no parameter a caller can get wrong: `immediate_context` and `ensure_staging` (`&ID3D11Device` IS the live-device guarantee), `send` and `ensure_sws` (only scalars, operating on the `AVFrame`/`SwsContext` the struct owns from its constructor to `Drop`), and `test_hw_device`, whose `# Safety` section described its own body — "calls the DXGI enumeration FFI and `make_device`" — while taking a single `u32`. `open_win_encoder` keeps its marker (raw `*mut AVBufferRef` pair) and gains the proof it lacked, including the null case the system path relies on: both refs may be null, the `is_null` guards keep `av_buffer_ref` off that path, and the refs it does take are new ones the codec context adopts. `D3d11Hw::new`'s proof records the ordering the multithread-protection fix depends on — the device store must precede `av_hwdevice_ctx_init`, which reads it. 14 fenced files -> 10. Verified on the Intel box .47: `-p pf-encode --features nvenc,amf-qsv,qsv` clippy `-D warnings` rc=0, the full Windows CI clippy set rc=0, pf-capture's 18 tests pass, and — because this is the live D3D11VA construct path, not dead code — `d3d11hw_alloc_drop_cycles` passes on real Intel silicon: 8 construct/drop cycles, no abort. Linux .21: fmt + both CI clippy steps rc=0. |
||
|
|
8e7ba00d2d |
refactor(encode/linux): third fence off — vaapi.rs, and VaapiHw::new needed no marker at all
Same criterion as the last two: `vaapi.rs`'s sites are pointer dereferences and libav ctx calls,
not ash, so a proof here carries an argument. Three regions, three arguments.
`VaapiHw::new` also loses its `unsafe fn` outright, and the contrast with its CUDA twin is the whole
point: `CudaHw::new` keeps the marker because it is HANDED a `CUcontext` the caller must vouch for,
while this one takes four scalars and opens the VAAPI device itself. Two functions of near-identical
shape, opposite answers, decided by whether a caller can supply something broken.
The other two regions keep their markers (`open_vaapi_encoder`/`_mode` are handed raw
`*mut AVBufferRef`s) and gain the proofs they lacked. The encoder-config block now records the fact
that makes it sound rather than obvious: `av_buffer_ref` returns a NEW reference the codec context
adopts, so the callee shares the caller's device/frames buffers instead of consuming them — which is
also why the low-power entrypoint ladder can retry with the same two pointers after a failed attempt.
The frames-pool block reuses the `CudaHw::new` argument: alloc returns null-or-initialized and
`AvBuffer::from_raw` rejects null, so the `?` leaves before any field store can run.
One call site dropped its `unsafe {}` and its comment with it — the comment argued that libav was
initialized, which is a real precondition of the call but not one a caller can violate, so it is now
a note rather than a contract.
14 fenced files -> 11. Verified on .21 (fmt + both CI clippy steps rc=0) and, because this is the
live VAAPI construct path rather than dead code, ON THE AMD 780M (.116, pf-build distrobox):
`vaapi_cpu_encode_smoke`, `dmabuf_inner_alloc_drop_cycles` and `vaapi_probe_smoke` all pass —
3 passed / 0 failed, H265 + AV1 probes still true in both 8- and 10-bit.
|
||
|
|
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.
|
||
|
|
d72822ced7 |
refactor(encode/linux): second fence off — CudaHw::new's pointer walk gets its proof
`linux/mod.rs`'s fifteen sites are the same kind as `video_vulkan.rs`'s, not the ash kind: nine raw pointer dereferences and six libav calls, all inside `CudaHw::new`, which had no `unsafe` block and therefore no proof of the one thing worth proving here — that the pointer chain it walks is live. The marker STAYS (`cu_ctx: *mut c_void` is a `CUcontext` the caller must supply valid). The body is now two blocks, one per phase, because there are two distinct arguments to make. Both turn on the same non-obvious fact: `av_hwdevice_ctx_alloc`/`av_hwframe_ctx_alloc` return null or a ref whose `data` libav has ALREADY initialized, and `AvBuffer::from_raw` rejects null — so the `?` leaves before any of the field stores below it can run. That is what makes the `(*dev_ctx)`/`(*fc)` writes in-bounds stores on live allocations rather than a hope, and it is exactly the reasoning that was missing. The device block also records the ordering constraint that was implicit: `cuda_ctx` must be stored BEFORE `av_hwdevice_ctx_init`, which reads it. Two files now need no exemption: 14 fenced -> 12. Both were removable for the same reason — their sites are pointer dereferences, where a proof carries an argument, unlike the ash backends where it could only restate the call. That is the criterion for which fence to attack next, not file size. Verified on .21: fmt + `clippy --workspace --all-targets -- -D warnings` + the feature-gated `-p pf-encode --features nvenc,vulkan-encode,pyrowave` step, all rc=0 with no allow in either file. |
||
|
|
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. |
||
|
|
5b2be889f9 |
refactor(capture/windows): two # Safety sections that stated no contract
The previous commit took the markers with no `# Safety` at all. These two HAVE one, and that is
what makes them worth naming: a section can exist and still describe nothing a caller can violate.
`resolve_render_adapter` takes **no arguments**. Its section read "calls DXGI factory/adapter
enumeration; returns owned COM objects or an error" — a summary of the body. With no parameters and
no globals touched, there is no way to call it wrongly.
`create_nv12` asked that "`device` must be a live D3D11 device". It takes `&ID3D11Device`, a
borrowed reference-counted COM wrapper, so the borrow itself is that guarantee — safe Rust cannot
produce a dangling one. The remainder ("the returned texture is owned by the caller") is an
ownership note, the same distinction `service::open_log_handle` already draws in its doc. Every
other parameter is a plain scalar.
Both bodies already had their explicit block, so again nothing moved. `create_nv12`'s proof said
"on the live `device` borrow (per the contract above)" and now says the borrow is what keeps it
live — a proof that pointed at a contract being deleted had to stop pointing at it.
The other four were read and KEPT, and they are the shape of a real one: `channel::send` and
`duplicate_and_deliver` take raw `HANDLE`s; `prepare_blend_scratch` requires the caller to hold the
slot's keyed mutex before it copies; `pyro_fence_signal` must run on the thread owning the immediate
context, which is load-bearing precisely because `IddPushCapturer` is `unsafe impl Send` and so CAN
be moved. `verify_is_wudfhost` (raw `HANDLE`, and the driver-channel security check) keeps its
marker too.
pf-capture: 21 `unsafe fn` -> 8, every survivor carrying an obligation a caller can actually break.
Verified on .47: pf-capture clippy `-D warnings` rc=0 + 18 Windows tests pass, host/pf-encode/
pf-vdisplay green; Linux .21 fmt + both CI clippy steps rc=0.
|
||
|
|
9e8473b46a |
refactor(capture/windows): eleven D3D11 helpers had no caller contract
The DXGI converters (`HdrP010Converter`, `BgraToYuvPlanes`, `VideoConverter`), the cursor blend
pass, and `create_ring_slots` declared their methods `unsafe fn` because their bodies are D3D11
FFI. That is a property of the body, not an obligation on the caller, and their own proofs said so:
"`?`-checked D3D11 methods on the live `device` borrow". Every parameter is either a borrowed
windows-rs COM wrapper (`&ID3D11Device`, `&ID3D11DeviceContext`, `&ID3D11Texture2D`, the views) or
a plain `u32`/`bool`/`DXGI_FORMAT`; each body builds its own descriptors from those and every
interface it creates owns its reference. There is nothing a caller can pass that makes them
unsound.
Not one of the eleven carried a `# Safety` section — the marker had no stated contract to inherit,
which is the tell. Each body was already one `unsafe {}` with its proof, so this is purely
subtractive: no block moved and no proof was written. The four call sites lost `unsafe {}` wrappers
whose own comments gave the game away — "`X::new` is `unsafe` (it compiles D3D11 shaders…)",
"`create_ring_slots` is an `unsafe fn` (it makes D3D11/DXGI COM calls)" — comments explaining that
the marker described the callee's body rather than anything the caller had to guarantee.
`compile_shader` KEEPS its `unsafe fn`: it takes `PCSTR`, a raw pointer the caller must guarantee
points at a NUL-terminated literal. That is the line — a contract a caller can actually break.
pf-zerocopy was surveyed the same way and deliberately left alone: its CUDA/Vulkan helpers look
identical by signature but state real obligations ("the shared context is current", "single-threaded
use of handles this bridge owns"), so signature screening finds candidates and only reading decides.
pf-capture already denies both `unsafe_op_in_unsafe_fn` and `undocumented_unsafe_blocks`, so the
crate stays fully enforced; the reduction is in obligations, not in warnings. It now has exactly one
`unsafe fn` left. Verified on the Intel box .47: pf-capture clippy `-D warnings` rc=0 and its 18
Windows tests pass, with host/pf-encode/pf-vdisplay clippy still green; Linux .21 fmt + both CI
clippy steps rc=0.
|
||
|
|
50531c8e9e |
fix(host/display): a screen picker that cannot stream a screen now says so instead of saving
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
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
release / apple (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
audit / cargo-audit (push) Canceled after 0s
audit / bun-audit (push) Canceled after 0s
arch / build-publish (push) Canceled after 0s
android / android (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
apple / swift (push) Canceled after 3m56s
The Windows console's "Streamed screen" picker SAVED and then did nothing. `capture_monitor` is
platform-neutral, so the PUT persisted; every consumer of it is Linux-gated, so a virtual display
was still created on connect. The operator got a control that acknowledged the click and changed
nothing — the worst of the three possible behaviours.
The root cause is not a missing gate, it is a missing BACKEND. Per-monitor capture is Linux/portal
only: `vdisplay::open`'s mirror arm is `#[cfg(target_os = "linux")]` because `pf-capture` has no
Windows entry point that can capture an arbitrary head. Its sole Windows entry point is
`open_idd_push`, a frame channel pushed by our OWN IddCx virtual display; DXGI Desktop Duplication
was deliberately REMOVED (`windows/dxgi.rs` keeps the GPU-preference hook only to stop DXGI
reparenting the virtual display off the pinned adapter, and says so). So there is nothing for a pin
to aim at, and exposing the picker got ahead of that.
Decision of record (user, this session): mark it unsupported now rather than build the Windows
backend here. A Windows mirror backend is a real feature gap and a project of its own — it needs a
duplication capturer plus everything the IDD-push path carries today (cursor sidechannel, 444,
10-bit, PyroWave, HDR) re-plumbed through it — not a follow-on to an enumeration commit.
Three places, so the answer is consistent wherever it is asked:
* `MonitorsResponse.pin_supported` — a CAPABILITY, reported by the build that would have to honor
the pin rather than sniffed from the OS client-side. When a Windows mirror backend lands this
flips and the console needs no change. `pinned` stays `None` off-Linux deliberately, and now says
why: it is what the console highlights as "sessions stream this", and highlighting a head nothing
will capture is the same lie in a different place.
* `enforced` drops `capture_monitor` off Linux. That list is exactly the "which controls are live vs.
coming soon" contract, and claiming this one unconditionally is what let the picker ship enabled.
* The PUT drops a non-Linux `capture_monitor` instead of storing it, and logs that it did. COERCED,
not rejected with a 400: this PUT is WHOLE-OBJECT, so a host that already stored a pin would have
every later settings save rejected over a field the operator cannot see — taking the other axes
down with it. This way such a policy self-heals on the next write.
Console: the picker renders read-only with an explanation, reusing the `envLocked` shape rather than
inventing a second one (`locked = envLocked || !pinSupported`). Managed heads were ALREADY excluded
from selection here (`mon.enabled && !mon.managed`), which is the "grey out, never filter blindly"
rule working as intended — the heads stay listed and explicable. `pin_supported` defaults to TRUE
when absent so an older host, which only ever shipped this picker where it worked, is not
retroactively locked out.
⚠️ `api/openapi.json` is hand-patched — punktfunk-host does not build on macOS, so the spec could not
be regenerated from the binary. The shape was verified by running the real generator over it: orval
emits `pin_supported: boolean` (required, no `?`) on `MonitorsResponse`.
Verified: `bun run codegen` (orval + paraglide + i18n parity: 434 messages, en + de), `bun run build`,
`bun run lint` (tsc --noEmit) all clean. Rust verified separately on .173 — see the next commit's
note if that lands, since neither xcheck target covers punktfunk-host.
|
||
|
|
ed021a13ee |
test(vdisplay): give the 3.2 case eyes — a subscriber, and the probe that splits the two candidates
§5 3.2 reproduces on glass and the shipped fix is not sufficient: the panel stays dark after teardown. Two candidates were left undistinguished by the first on-glass run — the adopted snapshot was already poisoned, or the dark-desk backstop never fired — because the case had no way to tell them apart. This adds that way. **The subscriber.** The adoption arm and `restore_displays_ccd`'s backstop announce themselves ONLY through `tracing`, and a bare `cargo test` harness installs no subscriber, so both events went nowhere. `tracing-subscriber` becomes a DEV-dependency of pf-vdisplay (the shipped host's closure through this crate is unchanged) and `init_test_tracing` wires it: `try_init` so a second live case in the same binary is a no-op rather than a panic, `with_test_writer` so it interleaves with the harness rather than racing `println!`, `RUST_LOG` still winning over the `debug` default. `live_inplace_resize` gets it too — its comment claimed tracing-subscriber "is not a dep of this crate, run the host binary for traced runs", which was the same blindness written down as a limitation. It no longer holds. **The probe is the actual discriminator, and it needs no logs at all.** Member 1's isolate is INJECTED to fail, so nothing of ours deactivates anything there. Sampling `active_physicals()` immediately after member 1's create therefore answers the question directly: * empty -> the arriving IddCx monitor took the desktop on its own. Every snapshot from that instant on records "panel off", so member 2's adopted snapshot is POISONED AT BIRTH and restoring it faithfully restores darkness. Adoption works; the snapshot SOURCE is the defect. * non-empty -> poisoning is excluded and the break is downstream: no adoption line means teardown's restore was never gated on, a backstop line with a non-zero force-EXTEND rc means the remedy itself failed. The failing assertion now reads that verdict out instead of asserting one cause, so the next on-glass run reports which link broke rather than only that one did. ⚠️ Still UNRUN on glass — this is the instrument, not the measurement. Needs .173's CONSOLE session (ssh is session 0 and CCD sees nothing there), and it can leave the desk dark: recover with `SDC_USE_DATABASE_CURRENT|SDC_APPLY`, not EXTEND, which returns rc=31 against a single display. Checked while writing this, and recorded so it is not re-derived: the "backstop is structurally inert on a one-display box" theory does NOT survive. Both real call sites restore BEFORE the virtual is REMOVEd, so two displays are connected and the preset applies — `live_force_extend_*` measured exactly that (1 -> 1 -> 2). The documented residual is narrower: a restore that fails once the virtual is already gone. Verified: `xcheck.sh windows clippy` clean (it passes `--all-targets`, so this test code really is compiled), `cargo fmt --all --check` clean. ⚠️ `Cargo.lock` moves with this commit and must not be split from it. CI runs `cargo clippy --workspace --all-targets --locked`, so a new dependency edge whose lockfile entry is missing does not degrade to a re-resolve — it FAILS the build outright. The dev-dependency adds one line (pf-vdisplay gains a `tracing-subscriber` edge; the package itself was already in the graph via punktfunk-host and pf-encode, so nothing new is vendored). |
||
|
|
8381fd73ef |
test(vdisplay): bound the 3.2 case's creates, and what it found on glass
A hang must fail the case, not wedge the box. The first attempt at this case hung inside `create`; killing the harness skipped every `Drop` and leaked an IddCx monitor, and a few of those exhaust the driver's slot pool — after which every later run wedges too and only a reboot clears it. Both creates now run on a worker thread with a 45 s budget, so the harness exits NORMALLY, which is what lets the driver reap the session. Re-run with matched host+drivers: no hang, no leak, and the box came back with the panel lit. ⭐ With that guard in, §5 3.2 REPRODUCED ON GLASS for the first time (.173, LG TV attached, host 0.21.0 + drivers 9.9.728.2241, first isolate injected to fail): physicals before : [(4352, "LG TV SSCR2 [HDMI]")] after member 1 (isolate INJECTED to fail): [(260, "punktfunk [punktfunk-virtual]")] after member 2 (isolate REAL) : [(260, …), (264, …)] physicals during : [] physicals after teardown : [] <-- the operator's panel, still dark So the shipped 3.2 fix — adopt the first SUCCESSFUL isolate as the group's restore snapshot — is necessary but NOT sufficient. Two candidate reasons, both worth checking before changing code: * The adopted snapshot is already POISONED. The panel went dark at member 1's create even though our isolate never ran (the injected failure), because the arriving IddCx monitor took the desktop on its own. Member 2's isolate then snapshotted a topology in which the physical was ALREADY inactive, so restoring it faithfully restores "TV off". This is the same poisoned-snapshot chain §5 already records from the field logs. * `restore_displays_ccd`'s dark-desk backstop did not rescue it either. Worth its own look, since |
||
|
|
22f2686680 |
feat(vdisplay/windows): the console can finally show the operator's real screens
`GET /display/monitors` returned an empty list on every Windows host, plus a LINUX error string:
`monitors::list` is a per-compositor dispatch whose non-Linux arm bails, and it never even got
that far because `detect()` was not cfg-gated — on Windows it fell through to an
`XDG_CURRENT_DESKTOP` sniff and failed with advice about setting `PUNKTFUNK_COMPOSITOR`, which the
handler puts VERBATIM into the response. So the console showed no physical screen and its only
explanation was Linux troubleshooting (sweep §13.17).
The data was there all along. `target_inventory()` already walks the CCD database; it now also
reports the geometry that walk had in hand — the driving source's position and mode, the GDI
device name, the refresh rational, and `primary`. No second CCD walker.
* `monitors::list_windows()` maps that onto `PhysicalMonitor`. Two fields cannot mean on Windows
what they mean on Linux and are reported honestly rather than invented: `scale` is always 1.0
(Windows scaling is per-monitor DPI applied per application, not a compositor-global logical
scale — so the geometry is PIXELS), and an INACTIVE head gets zeroed geometry because the CCD
mode indices are only valid for active paths. Inactive heads are still listed, per `list`'s own
contract, so "why can't I pick it?" keeps an answer.
* `managed` is finally truthful. The doc says only KWin can tell a virtual output from a real head;
Windows can too, via our own EDID manufacturer id in the device path (
|
||
|
|
735cd99e8f |
fix(ci/windows): the web install was the one CI site still running lifecycle scripts
apple / swift (push) Successful in 5m12s
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
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 / screenshots (push) Canceled after 0s
android / android (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
windows-host / package (push) Canceled after 12m40s
The Windows host job died in "Build + smoke-boot web console (bun)":
$ bun2nix -o bun.nix
error: bun is not installed in %PATH%
error: postinstall script from "punktfunk-web" exited with 255
bun install failed (255)
`web`'s `postinstall` is `bun2nix -o bun.nix` — Nix codegen that shells out to `bun` on PATH. This
job runs a fetched PORTABLE bun by absolute path (`$env:BUN_EXE`), so PATH has none and the script
aborted the install, taking the installer, the driver bundle and the publish steps with it.
Every other web install in CI already passes `--ignore-scripts` — ci.yml (twice),
web-screenshots.yml, sdk-publish.yml, plugin-kit-publish.yml, and the SDK install 25 lines further
down THIS SAME FILE. This one line was the lone exception: the same N-1-of-N seam asymmetry the
pf-vdisplay sweep keeps turning up. Nothing here needs those scripts — `bun run build` re-runs its
own `prebuild` codegen, and ci.yml proves the build works after an `--ignore-scripts` install —
and `bun.nix` is a Nix artifact this job neither consumes nor commits. deb.yml still regenerates
it (bun on PATH there), so the self-regenerating contract is unchanged.
⚠️ Honest about the evidence: I could NOT reproduce the failure on .173. Invoking the portable bun
by absolute path with `where bun` empty, the postinstall SUCCEEDED there — bun made itself
resolvable to its own lifecycle script, which the SYSTEM-account runner evidently does not. So this
is not a reproduced-then-fixed A/B. It is justified instead by construction: `--ignore-scripts`
removes the dependency on bun2nix (and on how bun2nix locates bun) altogether, and aligns the step
with every sibling. If the job goes green, that is the confirmation.
⚠️ Related trap for whoever deploys the console by hand: rebuilding `web/.output` does NOT take
effect until the `PunktfunkWeb` SCHEDULED TASK is restarted — restarting the host service does not
touch it. A server left running from before a rebuild serves HTML referencing the OLD asset hashes
while disk holds the new ones, so every asset 500s while `/login` still returns 200. Hit on .173
today; `Stop-ScheduledTask PunktfunkWeb; Start-ScheduledTask PunktfunkWeb` fixes it.
|
||
|
|
3d25482469 |
feat(client/windows): pinned host+profile tiles render in the grid
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m40s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m27s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m19s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m56s
apple / swift (push) Successful in 5m12s
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
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
android / android (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
The menu could pin since the last commit but nothing appeared, which is worse than not offering it. Each host's pins now render right after it, sharing its live status because they read the same record, with the profile named on an accent chip. A pin whose profile was deleted simply doesn't render. A pinned tile carries no menu of its own: it is a shortcut, not a second host, and pin/unpin already live on the primary tile's menu — the one place you decide it. Its hover key is `<fp>#<profile-id>` so two tiles for one host don't light up together. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
b220daf537 |
feat(client/windows): the settings screen edits profiles too
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m9s
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 1m19s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m53s
apple / swift (push) Successful in 5m13s
ci / docs-site (push) Successful in 1m4s
ci / web (push) Successful in 3m3s
ci / bench (push) Successful in 5m43s
deb / build-publish-host (push) Failing after 6m17s
arch / build-publish (push) Failing after 9m36s
ci / rust-arm64 (push) Failing after 9m41s
ci / rust (push) Failing after 9m44s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 32s
decky / build-publish (push) Successful in 43s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 25s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 24s
deb / build-publish (push) Successful in 8m52s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 17s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 16s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 21s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
android / android (push) Canceled after 13m2s
deb / build-publish-client-arm64 (push) Canceled after 6m50s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 2m47s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 2m42s
P1 on the WinUI shell. The same surface now edits either layer, switched by a combo above the section title — one settings UI, not a second editor that drifts from the first. The interesting difference from GTK is where the override gets recorded. This shell commits PER CONTROL, so it cannot hand over a list of touched rows the way a commit-on-close dialog can. Instead each control hands `SettingsOverlay::absorb` the effective settings before and after it fired, and absorb records the field that moved. The comparison is against what the control was SHOWING, not against the globals, so setting a value to whatever the global happens to be still records an override — the pin the design asks for — and nothing is ever removed by inference. ⚠️ The content key becomes `(scope, section)`. Keyed on section alone, switching scope diffs one scope's controls into the other's, which re-sets each reused ComboBox's items (clearing WinUI's selection) while skipping `selected_index` wherever the two scopes' values compare equal — the combos then render blank until touched. That is the same trap the section key already existed for, one axis wider. Only profileable rows render in profile scope: auto-wake, the library toggle, the decoder and GPU pickers and the forwarded-pad picker are facts about this device or this network, not about "Game vs Work". Two shapes follow from this toolkit rather than from the design. Creating a profile takes an auto-numbered name and is renamed from the row below, because ContentDialog here is text-only and cannot hold a text field — the same constraint that put the host editor in a tile. And Delete arms a root state that renders the confirmation declaratively, since dialogs here are elements with `is_open`, not calls. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
22cb975659 |
feat(client/linux): "Create shortcut…" — a double-clickable entry for a host or a pinned profile
ci / web (push) Successful in 1m11s
ci / rust-arm64 (push) Failing after 6m32s
arch / build-publish (push) Failing after 9m5s
ci / docs-site (push) Successful in 1m12s
deb / build-publish-host (push) Failing after 5m9s
android / android (push) Failing after 14m18s
deb / build-publish (push) Failing after 5m9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 18s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 19s
decky / build-publish (push) Successful in 26s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 13s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 16s
ci / bench (push) Successful in 6m30s
deb / build-publish-client-arm64 (push) Successful in 7m47s
apple / swift (push) Successful in 7m11s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 7m46s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 7m53s
flatpak / build-publish (push) Failing after 9m29s
ci / rust (push) Successful in 24m12s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
D1's last Linux piece (design/client-deep-links.md §5). The shortcut is a CONTAINER FOR A URL, not a second launch mechanism: it invokes the client with a positional `punktfunk://…`, the same door xdg-open and a browser prompt use. That is what makes it survive things it has no control over — scheme registration being lost, the host moving to a new address, the store being wiped — because the URL itself carries the stable id, the address and the pin. A pinned card writes its profile into the shortcut, so "Desktop · Work" on the app grid is one double-click to that exact session. Two sanitisation points that are not cosmetic: the filename is slugged to ASCII (a host called "Büro · Mac" must still produce a writable name), and the `Name=` value is flattened to one line — desktop entries are line-oriented, so a newline in a host name would end the key and turn the rest into another one. Under flatpak the sandbox cannot write ~/.local/share/applications, so the user gets the URL to place themselves — the fallback the design already sanctions. The `org.freedesktop.portal.DynamicLauncher` route, which exists for exactly this and shows its own confirmation, is the intended upgrade there and is not built yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bd0103b1ac |
feat(client/linux): a host edit sheet, and with it the clipboard toggle Linux never had
deb / build-publish (push) Canceled after 0s
android / android (push) Canceled after 1m27s
apple / swift (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 1m33s
ci / bench (push) Canceled after 0s
ci / web (push) Canceled after 47s
ci / rust (push) Canceled after 1m32s
ci / rust-arm64 (push) Canceled after 47s
ci / docs-site (push) Canceled after 46s
deb / build-publish-host (push) Canceled after 0s
deb / build-publish-client-arm64 (push) Canceled after 0s
decky / build-publish (push) Canceled after 2s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Canceled after 5s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 6s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 6s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Canceled after 6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 6s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
flatpak / build-publish (push) Successful in 7m18s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 9m7s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m3s
Linux had "Rename" and nothing else, so two per-host settings that already existed in the store and on the Apple and Windows clients had no surface here at all: the per-host clipboard opt-in, and now the profile binding. A Linux user could not turn on a feature they were already paying the storage for. The sheet holds exactly the things that are properties of the HOST rather than of the stream — its name, whether this machine shares its clipboard with it, and which settings profile a plain click uses. A binding whose profile was deleted reads as "Default settings" and is cleaned up on save, which is the same "dangling resolves as none" rule the connect path follows. The menu entry becomes "Edit…", and the function and message follow: a `rename_dialog` that opens an "Edit Host" sheet is exactly the drift that confuses the next reader. Pinning stays in the card menu rather than doubling as a list of toggles here — one place to pin is enough, and it is the place you are already looking when you decide to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b9a7163556 |
feat(client/linux): the speed test writes where the tested host reads, and cards can copy their link
android / android (push) Canceled after 4m16s
apple / swift (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 3m28s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bench (push) Canceled after 0s
ci / rust (push) Canceled after 2m42s
ci / rust-arm64 (push) Canceled after 2m41s
deb / build-publish (push) Canceled after 2s
deb / build-publish-host (push) Canceled after 2s
deb / build-publish-client-arm64 (push) Canceled after 3s
decky / build-publish (push) Canceled after 2s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Canceled after 6s
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 (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
flatpak / build-publish (push) Canceled after 1m5s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 1m7s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 46s
Two WP3 pieces.
**Speed test (§5.3).** Measuring the slow retro box downstairs used to re-tune the
desktop upstairs: Apply always wrote the GLOBAL bitrate, which the design calls the
sharpest existing symptom of the missing feature. It now writes the layer that host
actually resolves bitrate from — the bound profile's override if it has one, the global
if the host is unbound. A bound host whose profile inherits bitrate could legitimately
mean either, so it gets both buttons instead of a guess. The target depends only on the
host, so it is resolved before the test finishes and the button says where it will write
("Set in “Game”"). A one-off (a pinned card's profile) wins over the binding, and a
dangling binding falls back to the global — the same precedence a connect uses.
**Copy link.** Host and pinned cards can copy their own `punktfunk://` URL — the
self-emitted form carrying the stable id *and* host+fp, so a link pasted into a Playnite
entry or a Stream Deck macro still resolves after the host re-addresses or the store is
wiped. A pinned card copies its profile with it, which is the whole point of pinning it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
08ec0f4239 |
feat(client/linux): pin a host+profile as its own card
android / android (push) Canceled after 1s
apple / screenshots (push) Canceled after 0s
apple / swift (push) Canceled after 0s
arch / build-publish (push) Canceled after 4s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / rust (push) Canceled after 8s
ci / bench (push) Canceled after 0s
ci / rust-arm64 (push) Canceled after 5s
deb / build-publish (push) Canceled after 3s
deb / build-publish-host (push) Canceled after 0s
deb / build-publish-client-arm64 (push) Canceled after 0s
decky / build-publish (push) Canceled after 8s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 20s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 25s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 31s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 33s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m7s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 1m57s
flatpak / build-publish (push) Canceled after 1m44s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 4m11s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 4m11s
§5.2a: a profile you reach for regularly shouldn't live behind a context menu. Pinning one puts *Desktop · Work* in the grid next to *Desktop*, and clicking it is a plain one-click connect with that profile. The pin is presentation state on the host record — `pinned_profiles`, which has been in the store since P0 and unused until now. Deliberately NOT a duplicated host entry: that would fork pairing, WoL MACs and renames, and every host-level action would then need a "which card owns this" rule. So a pinned card is the same record rendered again with a profile attached: it shares the host's live status because it reads the same record, and a pin whose profile was deleted simply doesn't render. What each card offers follows from what it is. The primary card keeps the host actions and gains "Pin as card ▸" (listing only profiles not already pinned; "Default settings" isn't pinnable — the primary card is that). A pinned card offers the one-offs and its own removal, and deliberately not pair/rename/forget — it is a shortcut, not a second host, and offering destructive host actions there would blur the distinction. One action serves both directions: which kind of card it sits on decides whether it pins or unpins. Verified on .21: a host bound to "Game" renders its primary card with a neutral Game chip and, right after it, the pinned "Work" card with an accent chip, both showing the same online/paired state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b485f91f52 |
feat(client/linux): overridden rows say so, and can go back to inheriting
apple / swift (push) Successful in 5m3s
android / android (push) Canceled after 1s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 5s
ci / docs-site (push) Canceled after 0s
ci / bench (push) Canceled after 0s
ci / web (push) Canceled after 6s
ci / rust (push) Canceled after 6s
ci / rust-arm64 (push) Canceled after 7s
deb / build-publish-host (push) Canceled after 0s
deb / build-publish-client-arm64 (push) Canceled after 0s
deb / build-publish (push) Canceled after 1s
decky / 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
flatpak / build-publish (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 4s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
The gap in what the scope switcher shipped: a profile was a one-way door. Overrides are recorded on touch and never inferred from comparing values — which is the right model, and exactly why "not overridden" needs an explicit way back. Each overridden row now carries an accent dot in its prefix and a reset button in its suffix, so which settings a profile actually changes is legible without reading every value against the defaults. Reset queues the field, re-opens the dialog in the same scope and lets the close handler commit everything else first — so one code path still builds the rows, including the rebuilt ones showing the inherited value again. Clears are applied after the touched fields for a reason: resetting a ComboRow can fire its change handler on the way past, and "back to inheriting" is the later, explicit intent. An unknown field in the cleared set warns rather than silently doing nothing — this list and the write-back list have to stay in step. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
fa430646ff |
fix(packaging): the Pulse shim is never the client's, and never the host's requirement
android / android (push) Failing after 5m55s
ci / web (push) Successful in 1m1s
ci / docs-site (push) Successful in 1m10s
apple / swift (push) Successful in 5m22s
ci / rust-arm64 (push) Failing after 8m30s
ci / bench (push) Successful in 5m14s
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 23s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
arch / build-publish (push) Successful in 15m26s
deb / build-publish (push) Successful in 8m56s
ci / rust (push) Failing after 17m59s
deb / build-publish-host (push) Failing after 8m49s
apple / screenshots (push) Canceled after 0s
deb / build-publish-client-arm64 (push) Canceled after 9m1s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 4m14s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 6m31s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 3m36s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 28s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 30s
Same bug as the Arch package, in the other two packagings. The .deb CLIENT recommended `pipewire-pulse`. Apt installs Recommends by default and the shim Conflicts with `pulseaudio`, so this proposed pulling the user's sound server out from under them in exchange for something the client cannot use: audio.rs drives libpipewire-0.3 directly and opens no Pulse socket. (SDL is initialized with zero subsystems — gamepads only, on demand — so it brings no audio backend either.) The RPM HOST hard-Required `pipewire-pulseaudio`, which conflicts with `pulseaudio` on Fedora too — uninstallable for anyone running real PulseAudio. The host doesn't speak Pulse either; the shim is for the GAMES, which commonly emit through the PulseAudio API, and real PulseAudio serves those equally well. Demoted to Recommends, so the default Fedora box is unchanged and the informed choice stops being blocked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7d18468477 |
fix(packaging/arch): a client that never speaks Pulse must not demand the Pulse shim
`punktfunk-client` hard-depended on `pipewire-pulse`, which CONFLICTS with
`pulseaudio` — so anyone keeping real PulseAudio could not install the client at
all ("unresolvable package conflicts detected"), even though the client has no
use for the shim: audio.rs drives libpipewire-0.3 directly for both playback and
the mic uplink, and never opens a Pulse socket. Dropped it from the client.
The host doesn't speak Pulse either — it captures the sink monitor natively — but
the shim is genuinely useful there, for the GAMES, which commonly emit through
the PulseAudio API. That's an optdepend, and one real `pulseaudio` satisfies just
as well, so the host stops blocking that choice too.
Both now match what the .deb and the RPM already said (Recommends / "degrade
gracefully without it"); only the Arch package had hardened it into a conflict.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
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.
|
||
|
|
f40c90aa23 |
refactor(host/inject/win-display): the Win32 unsafe surface is the FFI call, not the function
`spawn_in_active_session` existed only to launder `spawn_inner`'s `unsafe fn` marker — and its own comment said why that marker was empty: the callee "has no caller-side preconditions, it validates the session/token itself and owns every handle it opens". A marker that documents the absence of a contract is not a contract. The two collapse into one safe fn whose `unsafe` now sits on the eight Win32 calls, each with the proof it actually needs. Kept `unsafe fn` exactly where a caller CAN break something: `merged_env_block` (a `*const u16` block it must trust), `spawn_host` (a borrowed job `HANDLE`), `desktop_name` (an `HDESK`), `inject_following_desktop` (a live synthetic-pointer device). Those bodies gain explicit blocks instead — `merged_env_block`'s pointer walk is the one place here where the proof is load-bearing rather than restating a signature, and it now says why the scan cannot run off the block's end. With those four files at zero, all three crates take `deny(unsafe_op_in_unsafe_fn)` at the root, next to the `undocumented_unsafe_blocks` deny they already had. The pair matters: alone, the older deny is satisfied by an `unsafe fn` body, which needs no blocks at all and so can hide an unproven FFI call. The workspace lint stays `warn` — it is a ratchet over the encoder backends still to be cleared, and a crate that reaches zero should not be able to drift back silently. Windows E0133 214 -> 182 (the remainder is the ash/AMF-dense encode backends). Verified on the Intel box .47 and on Linux .21: no errors, no `unused_unsafe`, no dead-code fallout. |
||
|
|
808eba7292 |
feat(client/linux): bind a host to a profile, or connect with one just this once
ci / web (push) Successful in 1m56s
ci / rust (push) Failing after 6m15s
ci / docs-site (push) Successful in 2m1s
android / android (push) Canceled after 11m36s
ci / rust-arm64 (push) Failing after 9m8s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 24s
decky / build-publish (push) Successful in 36s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 22s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 17s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 18s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
ci / bench (push) Successful in 5m23s
deb / build-publish-client-arm64 (push) Successful in 7m28s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 6m31s
deb / build-publish (push) Successful in 9m19s
arch / build-publish (push) Successful in 18m42s
flatpak / build-publish (push) Failing after 8m19s
deb / build-publish-host (push) Successful in 10m41s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m15s
apple / swift (push) Successful in 5m30s
docker / deploy-docs (push) Successful in 29s
docker / build-push-arm64cross (push) Successful in 11m22s
apple / screenshots (push) Successful in 27m32s
The other half of P1 on GTK (design/client-settings-profiles.md §5.2): until now a
profile could be created and edited but only *used* through `--profile` or a URL. Host
cards get two menus, and they are separate on purpose — the predictability rule is that
connecting with a profile must never change what the card does next time.
- **Connect with ▸** is a one-off. It rides `ConnectRequest.profile` into the session's
`--profile` flag and touches no stored state. "Default settings" in that menu is
`Some("")`, not `None`: on a bound host, asking for the defaults is a real choice and
has to survive as a value rather than degrade into "use the binding".
- **Default profile ▸** is the explicit rebinding act. It writes straight onto the host
record — the binding IS a field there, so there is no map to keep in step — matched by
fingerprint, else by address, like every other per-host lookup in this client.
- The card carries a **chip** naming its bound profile, so what a plain click will do is
visible without opening a menu. A binding whose profile was deleted shows nothing and
connects with the defaults, which is exactly what the resolver does.
A URL's `profile=` now takes the same one-off route, since it is the same kind of thing.
Verified on .21: `punktfunk://connect/Bound%20Box?profile=Work` against a host bound to
"Game" spawned a session that logged `profile=Work id=bbbbbbbbbbbb` — the one-off won,
and the binding on disk was untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
37a42078a0 |
fix(xcheck): mirror [workspace.lints] too — and the two things that hid behind it
windows-host / package (push) Failing after 5m0s
windows-host / winget-source (push) Skipped
ci / rust-arm64 (push) Failing after 22s
ci / web (push) Successful in 2m28s
ci / docs-site (push) Successful in 2m55s
ci / rust (push) Failing after 6m19s
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 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 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
ci / bench (push) Successful in 6m20s
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 12s
android / android (push) Successful in 12m2s
deb / build-publish-client-arm64 (push) Successful in 8m59s
arch / build-publish (push) Successful in 14m57s
deb / build-publish (push) Successful in 12m3s
deb / build-publish-host (push) Successful in 13m33s
apple / swift (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 14m27s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 14m22s
`scripts/xcheck.sh` stopped working the moment the workspace adopted its unsafe discipline
(`52191071`). That commit added `[lints] workspace = true` to every crate manifest, and a member
inheriting a lint table the workspace ROOT does not define does not merely lose the lint — cargo
refuses to parse the manifest at all:
error inheriting `lints` from workspace root manifest's `workspace.lints`
`workspace.lints` was not defined
The generated root mirrored `[workspace.package]` but nothing else. It now mirrors every
`[workspace.lints.*]` table generically, so a future table (clippy, rustdoc, …) is picked up
without touching the script again.
That mattered more than a broken helper, because xcheck is the ONLY thing that lints these crates
for Windows — the Windows CI job clippies `-p punktfunk-host -p punktfunk-tray`, which does not
lint a dependency. So while it was down, two real failures sat on main unseen:
* **pf-frame `dxgi.rs`, 8 × `clippy::undocumented_unsafe_blocks`.** `307ca88b`/`0a710fdf` split
the D3DKMT/DXGI calls into one `unsafe` block per FFI call under a single shared SAFETY comment,
but the lint wants a proof per block — a comment two lines up with an intervening statement does
not count. Each site now carries its own. Not "as above", which this sweep already found to be a
proof that rots when the block it points at moves.
* **pf-win-display `input_desktop.rs`, one E0133.** `GetUserObjectInformationW` is an unsafe
operation in an `unsafe fn` body, which `unsafe_op_in_unsafe_fn` (now warn workspace-wide, and
`-D warnings` here) requires to sit in its own block. The SAFETY proof was already written; only
the block was missing.
The `&&` chain in `elevate_process_gpu_priority` keeps two separate blocks rather than one around
the chain — wrapping it would destroy the short-circuit and always issue the relative-priority
call. Same trap this sweep hit in `wait_mode_settled`.
Verified: both xcheck targets clean again.
|
||
|
|
21eda37aa3 |
fix(win-display): punktfunk's own display is not one of the operator's panels
The IddCx driver declares `DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI` for its monitors
(`packaging/windows/drivers/pf-vdisplay/src/monitor.rs`, `IDDCX_MONITOR_INFO::MonitorType`),
and `output_tech_class`'s allowlist reads HDMI as a real external panel. So every consumer of
`target_inventory` counted OUR OWN virtual display among the operator's physical monitors.
Measured on .173: both targets came back `external_physical`, `[(4352, "LG TV SSCR2", HDMI),
(257, "punktfunk", HDMI)]`.
That is not cosmetic. `restore_displays_ccd` ends with the last-resort guarantee that the desk
is never left all-dark:
let (connected, lit) = target_inventory().filter(external_physical) …
if connected > 0 && lit == 0 { force_extend_topology() }
The restore runs BEFORE the virtual is REMOVEd, so our own still-active display kept `lit >= 1`
and the backstop could NEVER fire — in exactly the situation it was written for. It also made
our display a candidate "physical suspect" in the disturbance-attribution inventory, which
`TargetInventory`'s own doc rules out ("only indirect/virtual targets (our own IDD included)").
Targets carrying our EDID manufacturer id ("PNK", stamped by the driver's edid.rs) are now
classified `external_physical = false`, tech `punktfunk-virtual`. Matching on the monitor device
path rather than the friendly name — the path carries the manufacturer id the OS itself derived
— and on both spellings, since the CCD device-interface path uses `#`
(`\\?\DISPLAY#PNK0000#…`) while the PnP instance id for the same monitor uses `\`
(`DISPLAY\PNK0000\1&15ecd195&5&UID264`, observed on .173). Allowlist-shaped like
`output_tech_class`, so a third-party virtual display is never silently adopted.
ON GLASS, read-only, on .173 — and it caught the backstop's trigger live:
target 264 active=true external_physical=false tech=punktfunk-virtual "punktfunk"
target 4352 active=false external_physical=true tech=HDMI "LG TV SSCR2"
The physical is dark while our virtual holds the desktop. Post-fix that is `connected=1, lit=0`
— the backstop fires. Pre-fix it was `connected=2, lit=1` — suppressed. The new hardware case is
deliberately READ-ONLY (creates and destroys nothing) so it is safe against a live host;
repeated IddCx create/destroy churn is what wedges the driver's slot pool.
Also adds `manager::FAIL_NEXT_ISOLATES`, a `#[cfg(test)]` seam that fails the next N
`isolate_displays_ccd` calls, with every call site in manager.rs routed through it. The Phase-3
recovery legs fire only on a failed isolate, which real hardware does not produce, so they
cannot otherwise be exercised. `#[cfg(test)]` rather than an env knob: this crate's live tests
compile under `cfg(test)`, so no production switch exists that could disable display isolation
on an operator's box. The §5 3.2 case built on it is included but is NOT yet a passing on-glass
result — see the plan for where it got to.
Verified on .173: clippy -p punktfunk-host -p pf-vdisplay --all-targets and -p pf-win-display
--all-targets clean; pf-vdisplay 56 pass, pf-win-display 2 pass + hardware cases; both xcheck
targets clean.
|
||
|
|
2b2c6f045d |
test(vdisplay): pin the force-EXTEND backstop on glass, with a real panel attached
`force_extend_topology` does two jobs — stop a fresh IddCx monitor being CLONED onto the existing panel, and serve as `restore_displays_ccd`'s last-resort "the desk is not left dark" backstop. Probed directly on .173 with only the LG TV connected, its preset returns rc=31 ERROR_GEN_FAILURE while SDC_USE_DATABASE_CURRENT returns 0, which reads like a backstop that cannot back anything up. On glass it is not, and this case is the measurement that settled it. Active paths went 1 -> (virtual up) 1 -> (after force-EXTEND) 2: with one connected display there is nothing to extend across, hence rc=31; with the virtual present there are two and the preset applies. Both real call sites run in exactly that state — the restore fires BEFORE the REMOVE, so the virtual is still there. No defect. Recording it so nobody re-derives the alarming half from the rc alone. ⭐ It also caught the clone hazard live: the arriving virtual monitor did NOT get its own active path (1 -> 1); only the forced EXTEND gave it one (-> 2). That is exactly the "no distinct source -> no frames" case the function's own doc describes, observed rather than assumed. ⚠️ Residual, noted not asserted: a restore that fails once the virtual is already gone is back to one connected display, where EXTEND returns 31 and cannot re-light anything. Context for the sweep's other on-glass claims: the earlier `live_create_drop` and `live_inplace_resize` passes on this box were taken with the TV POWERED OFF (every monitor devnode Code 45), so they proved the driver round trip but never exercised deactivating a real panel. Re-run now with the TV attached, `live_inplace_resize` still reports in_place=true (target 257 -> 257, 2356x1332), so Phase 6.2 holds with a physical present. Verified on .173: clippy -p punktfunk-host -p pf-vdisplay --all-targets and -p pf-win-display --all-targets clean, 56 pass + 3 ignored, no orphans. |
||
|
|
ea9a25d3b1 |
fix(win-display): a host with nothing lit is an ANSWER, not a failed CCD query
`query_active_config` asked `QueryDisplayConfig` for the active topology even when the sizing call had just answered `numPaths = 0`. Windows rejects a zero-count query rather than handing back an empty set, so "nothing is active" came back as "the query failed". Zero active paths is an ordinary state, not a broken one: every panel off or in standby, a KVM switched away, a headless box between its adapter arriving and its first monitor. The `None` propagated into the two places that matter — `count_other_active` reported failure where the honest answer is `Some(0)`, and `isolate_displays_ccd` bailed at its first line, which is exactly the teardown-gate condition whose recovery legs exist to stop the operator's physical panels being left dark (sweep §5 3.1). MEASURED on .173 (RTX 4090, Win11 26200) with the TV powered off — every monitor devnode Code 45, `numPaths = 0` for `QDC_ALL_PATHS` too — in a live, logged-on CONSOLE session: `GetDisplayConfigBufferSizes` returns 0 paths and `QueryDisplayConfig` then returns 0x57 ERROR_INVALID_PARAMETER (0x5 ERROR_ACCESS_DENIED from session 0). This is the mechanism behind the plan's ⭐ note that `count_other_active(&[])` yields `None` on that box: not the API refusing us, just a query nobody should have made. Three changes, one defect: * `query_active_config` short-circuits `numPaths == 0` to an empty-but-valid config. * `isolate_displays_ccd` returns that empty snapshot straight away instead of entering its retry loop. Nothing is active, so nothing needs deactivating and nothing needs restoring; the loop's re-commit exists to drive the IddCx adapter's COMMIT_MODES, and with no path at all there is no config to commit — four attempts would each log a rejected apply and sleep 250 ms to reach the topology we already have. `restore_displays_ccd` already treated an empty config as the no-op it is, so the round trip stays consistent. * `set_virtual_primary_ccd` goes through the shared query. It carried a verbatim copy of it — same flags, same shape — and was therefore the one CCD entry point that would not have inherited the fix. Same seam asymmetry this crate keeps producing, and two hand-written unsafe blocks go with it. Also this file's first tests. The case is `#[ignore]`d hardware, so it reports `ignored` rather than a vacuous `ok` when nobody runs it. On-glass A/B in the console session on .173: it passes with the short-circuit and FAILS without it, naming the conflation. A box that does have a display lit passes either way. ⚠️ The test is read-only and must stay that way — the obvious companion assertion, `isolate_displays_ccd(&[])`, is deliberately not written: an empty keep set means "keep nothing", so on a box with displays it would deactivate every one of them. Verified on .173: `clippy -p punktfunk-host -p pf-vdisplay --all-targets` and `clippy -p pf-win-display --all-targets` both clean, pf-vdisplay 56 pass, pf-win-display 1 pass + 1 ignored. Both xcheck targets clean. |
||
|
|
5a14d2b3f4 |
fix(vdisplay): the helper budget must end the tree, not just the process we spawned
`Child::kill` is one `TerminateProcess`: it ends exactly the process we launched. On Unix
that is the whole story here — `kscreen-doctor`, `systemctl`, `pw-dump` are single processes
we exec directly. On Windows there is no direct exec, so every helper is reached through a
shell (`cmd /c …`, `powershell -Command "… | pnputil …"`) and the process that actually
hangs is a GRANDCHILD. Killing the shell left it running, holding the stdio handles and the
working directory it inherited from us, which means the budget bounded nothing.
Not theoretical: it is why a fully green `cargo test -p pf-vdisplay` still failed its CI job
on `33a31427`. The suite's own hung-helper case orphaned a 60-second `ping.exe`; it kept the
build step's stdout pipe open past the runner's 10 s WaitDelay ("exec: WaitDelay expired
before I/O complete") and pinned `crates\pf-vdisplay` so the workspace could not be cleaned
up. The tests reported `55 passed; 0 failed` in the same log.
Spawned children are now enrolled in a Job object. Job membership is inherited across
`CreateProcess`, so one `TerminateJobObject` ends every descendant, and
`JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` makes that hold on paths that never reach the explicit
call — an early `?`, a panic — because closing the last handle is then itself the kill.
Best-effort: a box that cannot make a job object degrades to the single-process kill it did
before rather than failing the query, the same stance as the already-ignored `Child::kill`.
`output_within` ends the tree BEFORE `wait_with_output`. That read runs to EOF, and a
grandchild outliving the helper holds the write end — the one way this "bounded" helper
could still hang forever.
The new test is the permanent lever, and it is not vacuous: it asserts the grandchild
reached its first marker before asserting it never reached its second. Verified on .173
(Win11 26200): 56 pass, `clippy -p punktfunk-host -p pf-vdisplay --all-targets` clean, no
`ping.exe` survivors. A/B with the job disabled: the test fails with "a grandchild outlived
the budget" and one orphan survives. On the CI runner .133 the job APIs were also checked
directly, as Administrator (already inside a job — nesting works) and as SYSTEM: both kill
the tree.
Trap for anyone editing that test: a .cmd file is read in the OEM code page, so an absolute
path baked into it is mangled the moment the temp dir holds a non-ASCII character
(`C:\Users\Enrico Bühler\…` arrives as `B?hler`) and every redirect fails with "path not
found". It uses `%~dp0` instead, so the file stays pure ASCII.
|
||
|
|
cf419d8b6f |
feat(client/linux): the settings dialog edits profiles, not just the defaults
ci / web (push) Successful in 2m12s
ci / docs-site (push) Successful in 1m12s
android / android (push) Successful in 12m14s
ci / rust (push) Failing after 9m46s
ci / rust-arm64 (push) Failing after 9m43s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 18s
decky / build-publish (push) Successful in 32s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 14s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 12s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
ci / bench (push) Successful in 7m57s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m13s
deb / build-publish-client-arm64 (push) Successful in 7m40s
deb / build-publish (push) Successful in 9m5s
deb / build-publish-host (push) Successful in 10m19s
flatpak / build-publish (push) Failing after 8m34s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 7m44s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 8m10s
arch / build-publish (push) Successful in 21m3s
apple / swift (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
P1's core on GTK (design/client-settings-profiles.md §5.1). The same surface now edits either layer: a scope switcher at the top of General swaps between "Default settings" and one profile's overrides. Deliberately not a second editor — a parallel one would drift from this one field by field, which is exactly what the "one settings UI" principle is there to prevent. Three decisions carry the design's semantics into this file: - Rows always show the EFFECTIVE value — the global underneath with the profile's overrides applied — so a row the profile doesn't touch reads as the live global. That is what "inherit by default" has to look like. - Overrides are recorded ON TOUCH, never by diffing values at save time. A profile may therefore pin a value that happens to equal today's global and keep it when the global later moves. The handlers that record this are installed after the seed pass, or opening a profile would override every row in it. - Only profileable rows render in profile scope. Auto-wake, the library toggle, the decoder and GPU pickers, the audio endpoints, the forwarded-pad picker and the detected pad list are facts about this device or this network, not about "Game vs Work" (§3's tier G/H), so they simply aren't there. Switching scope closes the dialog — which commits the layer being edited — and asks the app to re-open in the new one, so there is a single code path that builds the rows and no ambiguity about commit ordering. Both scopes read the controls through one shared closure; the tri-state resolution row is the obvious trap if they ever diverged. Create / Rename / Duplicate / Delete live in the switcher's group. Names are unique case-insensitively and the prompt refuses duplicates in place rather than after the dialog is gone. Delete counts what actually breaks — hosts that fall back to the defaults, pinned cards that disappear — and leaves bindings dangling on purpose: they resolve as "no profile" everywhere, and rewriting every host record here would be a second, racier source of truth. `PUNKTFUNK_SHOT_SETTINGS_SCOPE` puts the screenshot scene into profile scope, so both halves of the surface are capturable. Verified on .21 under Xvfb: the switcher, the per-profile actions and the tier filtering render, and the Statistics row seeds to the Game profile's "Detailed" rather than the global's value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |