a87fbebc0a29116bb2dcf98b46e15727f22ca5ba
1232
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a87fbebc0a |
feat(client/linux): the client says whether it is current, and can update itself
`punktfunk-client --check-update` fetches the same Ed25519-signed per-channel manifest the host trusts, works out how this client was installed, and reports what — if anything — can be done about a newer build. `--apply-update` does the part it owns. `--version` prints the CI-stamped build string, which is both what the comparison needs and what a packaging gate can run the binary for. The engine lives here rather than in the Decky plugin for two reasons: verifying a signature needs crypto that Decky's embedded Python does not have, and the plugin is not the only surface that wants the answer. Every UI becomes a caller, exactly as it already is for --pair, --library and --list-hosts. What is possible depends on the install, and the module refuses to pretend otherwise. A flatpak is a per-user install its launcher can update. A .deb/.rpm/ pacman client goes through the packaged root helper — a fixed, parameterless systemd oneshot polkit authorises for members of the punktfunk-update group, so the request carries nothing an attacker could influence and the payload comes from the distro's own signed repositories. A sysext, a nix profile or a source build gets the command and nothing else: the signed sysext feed carries the HOST image, so a client sysext has no feed to update from, and a button that can only fail is worse than one honest line. A failed check reports the failure. "Could not tell" rendering as "up to date" is the one answer that must never happen. The box's capabilities are probed once into a `Caps` struct so the routing rules are a pure function of (install kind, caps) — all seven of them are tested without a box, including that the kill switch beats every kind and that pacman needs its own root-owned full-sysupgrade opt-in on top of the group. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f6cfe382fd |
refactor(update): one signed-manifest checker, shared by the host and the client
The host has known how to answer "is a newer build available for this box's channel?" since the update-from-web-console work. The Linux client is about to need the same answer from the same signed document, and a trust rule that lives in two places is a trust rule that will drift. So the parts where being wrong is a security bug now exist exactly once, in the new `pf-update-check`: Ed25519 verification against pinned keys, the manifest schema and its fail-closed validation, the post-redirect fetch, the version comparison that has to reconcile four different canary spellings, and the install-kind ladder — the last parameterised by which product is asking, since the delivery channels are the same ones but the markers are not. The pinned key list moves with them. Two lists could have disagreed about who may announce a release, and the one that drifted is the one nobody would have noticed; `publish-update-manifest.sh` follows to the new path, and a MISSING keys file there is now fatal rather than a warning that silently skips the cross-check the step exists to perform. Host call sites are unchanged throughout: `store::index` and `update::manifest` re-export from the shared crate under their old names, and `update::detect` keeps its cached `detect()` and the host's command hints. Verified with clippy -D warnings on Linux and on Windows (nvenc,amf-qsv,qsv), 340 host tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
84c6938562 |
test(inject): the uhid sweep tests learn the devnode-churn grace
arch / build-publish (push) Failing after 6s
ci / docs-site (push) Successful in 1m53s
ci / web (push) Successful in 1m54s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 8s
ci / rust-arm64 (push) Successful in 2m15s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 9s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 35s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 15s
deb / build-publish-client-arm64 (push) Successful in 1m32s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 28s
docker / builders-arm64cross (push) Successful in 9s
deb / build-publish (push) Successful in 3m50s
docker / deploy-docs (push) Successful in 34s
deb / build-publish-host (push) Successful in 4m26s
apple / swift (push) Successful in 4m54s
android / android (push) Successful in 7m2s
ci / rust (push) Successful in 9m15s
windows-host / package (push) Successful in 19m29s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 16s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m18s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m39s
apple / screenshots (push) Successful in 20m39s
|
||
|
|
6b0bd59b8d |
fix(gamepad): a mask glitch no longer flaps devnodes — the unplug sweep gets a grace
ci / docs-site (push) Successful in 1m12s
ci / web (push) Successful in 1m26s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 27s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 8s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 5s
ci / rust-arm64 (push) Successful in 2m14s
deb / build-publish-client-arm64 (push) Successful in 1m6s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 7s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 13s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 13s
deb / build-publish (push) Successful in 3m38s
docker / builders-arm64cross (push) Successful in 7s
docker / deploy-docs (push) Successful in 25s
deb / build-publish-host (push) Successful in 6m44s
android / android (push) Successful in 7m55s
ci / rust (push) Failing after 8m1s
windows-drivers / probe-and-proto (push) Successful in 30s
apple / swift (push) Successful in 4m43s
windows-drivers / driver-build (push) Successful in 1m47s
arch / build-publish (push) Failing after 5m39s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m29s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m45s
windows-host / package (push) Successful in 12m1s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 19s
apple / screenshots (push) Successful in 20m47s
The unplug sweep dropped a pad the instant its active_mask bit read clear — but a pad teardown is a whole PnP device removal (system-wide device-change broadcasts, and on re-create a full hidclass re-enumeration), so a client-side mask glitch of a few state frames cycled real devnodes. The sweep now drops a pad only after its bit has stayed clear for 300 ms; a bit that returns inside the grace disarms it. A real unplug tears down one grace later, which nobody observes — the pad already went quiet. Lands once in PadSlots, so every backend (uinput/uhid/XUSB/UMDF) inherits it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ccf5c922ee |
fix(core/abi): report_phase joins its siblings behind the quic cfg — the C header parses again
ci / docs-site (push) Successful in 1m6s
deb / build-publish-host (push) Failing after 14s
ci / web (push) Successful in 2m28s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 11s
deb / build-publish-client-arm64 (push) Successful in 1m16s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 10s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 10s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 12s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 16s
ci / rust-arm64 (push) Successful in 4m30s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m13s
apple / swift (push) Successful in 4m46s
android / android (push) Successful in 5m32s
docker / builders-arm64cross (push) Successful in 11s
deb / build-publish (push) Successful in 6m8s
docker / deploy-docs (push) Successful in 35s
ci / rust (push) Successful in 7m7s
arch / build-publish (push) Failing after 9m7s
flatpak / build-publish (push) Successful in 7m3s
windows-host / package (push) Successful in 18m52s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 12s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m25s
apple / screenshots (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 16m55s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m17s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m23s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 2m50s
release / apple (push) Successful in 25m47s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 4m44s
The new punktfunk_connection_report_phase lacked the #[cfg(feature = "quic")] its connection-fn siblings carry, so cbindgen emitted it OUTSIDE the guarded region that declares the opaque PunktfunkConnection — the generated header stopped compiling as C (CI's harness caught it; my local harness failure was a pre-existing macOS linker issue that masked this distinct cause — full-suite comparison attributed the wrong root). Verified: the harness compiles and passes in the amd64 CI-image container. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fa822744ff |
feat(core/host/android): phase-locked capture — frames arrive on the client's latch schedule
ci / web (push) Successful in 1m59s
ci / docs-site (push) Successful in 2m13s
ci / rust-arm64 (push) Successful in 5m17s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 5s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 4s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 4s
android / android (push) Successful in 5m52s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
deb / build-publish (push) Successful in 5m38s
ci / rust (push) Failing after 5m52s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 18s
arch / build-publish (push) Failing after 6m7s
deb / build-publish-client-arm64 (push) Successful in 3m55s
docker / builders-arm64cross (push) Successful in 19s
deb / build-publish-host (push) Failing after 4m24s
apple / swift (push) Successful in 4m35s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m13s
docker / deploy-docs (push) Successful in 31s
windows-host / package (push) Failing after 6m22s
windows-host / canary-manifest (push) Skipped
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 2m35s
flatpak / build-publish (push) Successful in 6m33s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m22s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 10m8s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 2m56s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 11m43s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 4m1s
release / apple (push) Successful in 25m5s
apple / screenshots (push) Successful in 20m50s
The host's capture tick and a client's panel vsync are independent ~120 Hz oscillators; their drifting phase sweeps every frame's wait-for-latch across a full refresh period (measured on-glass: latch p50 oscillating 5.4-8.9 ms with a fixed margin) — the beat is the residual judder and the fat p95, and no client can fix it alone. Design: punktfunk-planning design/phase-locked-capture.md. Protocol (punktfunk-core): - PhaseReport (control 0x32, next to the clock family): the client's next display latch ALREADY CONVERTED to host clock (the skew offset lives only client-side), panel period, uncertainty, and the measured median arrival-lead — the controller's error signal. ~1 Hz, latest-wins. CLIENT_CAP_PHASE_LOCK advertises it; CtrlRequest::Phase + report_phase() + the C ABI mirror carry it. - The 0xCF host-timing tail grows a phase ACK (applied_phase_ns, 29-byte form) under the same strict-prefix append discipline — old readers parse the shorter forms; degradation pinned by tests. Host engine (arrival-slaved loop — no backend can move the source vsync, per the tick-ownership audit in the design doc): - PhaseCtl bridges control task → encode loop (the fec_target pattern, multi-field). PhaseController walks a per-frame HOLD before submit toward the client's reported lead hitting target = max(2.5 ms, uncertainty+1ms): 1 Hz adjust, 2 ms max step, 300 µs deadband, period-wrapping (the newest-wins capture slot makes a wrapped hold sample fresher content, not staler). A loop local, so every mid-stream rebuild keeps the lock; a new session re-acquires. PUNKTFUNK_PHASE_LOCK=0 disarms. Android reporter: the presenter's 1 Hz pf.present flush returns the window's measured latch p50; the async loop converts the vsync clock's next timeline monotonic→realtime→host and reports. Inert toward old hosts. Gates: docker amd64 clippy --all-targets -D warnings (host+core, nvenc) clean; core suite incl. the new wire tests; cargo ndk arm64 check/clippy clean. On-glass A/B vs the .173 host owed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
98147fb89c |
fix(gamepad/windows): the channel proof sizes its feature buffer from the descriptor
`HidD_GetFeature`/`HidD_SetFeature` reject any buffer shorter than the collection's `FeatureReportByteLength`, and the proof hardcoded 64. That held only because every pad enumerated with the DualSense descriptor; a Deck that enumerates as a Deck reports 65 (its one feature report is unnumbered and 64 bytes wide, plus the report-id slot Windows always reserves), so both calls would have failed, the proof with them, and — the gate being fail-closed — the pad would never receive its DATA section at all. Take the length from the descriptor, and accept the Deck's reply at either offset 0 or 1: one driver binary serves four identities and Windows places an unnumbered report's payload behind the report-id slot. An answer must also VALIDATE, not merely parse. `from_feature_report` reinterprets any 17 bytes, and a Deck serves its one unnumbered feature report for *any* requested id — so it answers the PS `0x85` probe with Steam attribute bytes that parse into a proof and fail on magic. Returning that first answer stopped the search while the real proof sat one transport away. |
||
|
|
00c29f82f2 |
fix(gamepad/windows): a pad enumerates as the controller it IS, not always a DualSense
A Steam Deck client streaming to a Windows host had a stuck stick and a stuck d-pad. The virtual pad was enumerating with the DualSense VID/PID **and the DualSense report descriptor**, so Windows parsed the 64-byte Deck frame as DualSense report 0x01: LX = report[1] = 0x00 (stick hard left), LY = report[2] = 0x09 (hard up), and a d-pad hat of 0 — which is UP, held forever. The driver picked its identity from `device_type` in the sealed section, but hidclass asks for the descriptors and attributes while it STARTS the device, and the section can only be delivered over the HID device interface — which does not exist until those queries are answered. The channel was structurally unavailable at the only moment it was needed, so `device_type()`'s bounded wait always timed out and every identity fell back to DualSense. Not a race: DualShock 4 and the Edge enumerated as DualSenses too (verified on .173 — both report 054C:0CE6 with a 64-byte DualSense input report, while their on-demand strings read correctly). The devnode's own hardware ids carry the identity and are readable at EvtDeviceAdd, before anything is asked, so resolve it there. The section stays authoritative once attached; the old wait survives only for a devnode whose ids match nothing. `hwid_devtype_table_matches_the_driver` pins the host's hwid → device_type mapping against the driver's table, including the ordering trap that `pf_dualsense` is a prefix of `pf_dualsenseedge`. |
||
|
|
85dd75437b |
feat(driver-proto): an EDID detailed-timing builder for the session's mode
An 18-byte DTD with fixed reduced blanking (80 px / 45 lines — a virtual display's blanking only has to be self-consistent), refusing modes the encoding cannot carry (pixel clock past the u16 10 kHz field, 4K120-class; >12-bit actives). Lives here rather than in the driver so it is unit- tested on every platform; pf-vdisplay splices it over its hard-coded 1080p60 preferred-timing descriptor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8140d3f8b3 |
feat(capture/windows): a degraded stretch logs one summary line at recovery
Per-hole stall lines gate on prior ACTIVE flow, so inside a sustained ~2 fps phase only the first hole is reported and the log goes quiet exactly while the user suffers — the field shape (deep 15 s stretches at 2 fps) was invisible without a stats recording. StallWatch now tracks the stretch: opened by a reported stall, fed by every stall-sized hole while the activity gate stays broken, closed when sustained flow returns (or a ring recreate cuts it — its holes predate the recreate and still count). Closure surfaces one INFO line: span, hole count, summed hole time, worst hole. One-hole stretches dissolve silently (the stall's own line covers them), and a ≥10 s gap closes the stretch first so a quit-to-desktop pause never folds into the tally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
789dafc95f |
feat(capture/windows): the stall micro-probes get an off switch, the depth pin a backoff
Two standing costs from the interval-stutter investigation, consolidated: - PUNKTFUNK_STALL_PROBES=0 now opts a box out of the micro-probe engine (per-GPU 10 Hz fence copies, a parked blocking-DwmFlush waiter, the 5 ms-cadence CPU sentinel). Default stays ON while the field program runs; off, stall lines keep the driver telemetry and the cheap ETW present/queue discriminator and only lose the corroborating probe legs — the verdict matrix already treats an absent probe window honestly. - The negotiated-depth pin-back stops hammering a display that refuses the flip: it was 4 CCD writes + 8 display-config queries per second, forever, all on the session-global display-config lock. After ~2 s of eager retries it re-attempts every ~4 s — still self-healing, without the drumbeat. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ecb3d1ab22 |
fix(host/windows): the pad service thread no longer waits on pnputil at its leisure
driver_store_inventory() ran `pnputil /enum-drivers` synchronously inside the attach-failure diagnose path — on the pad service thread, with no timeout. A busy or wedged driver store blocks that enumeration for tens of seconds, and the thread must keep draining pad slots. The query now runs on its own thread (spawned once per process); the caller waits a bounded 2 s so a fast pnputil still lands in the same report, and a slower one fills the cache for later diagnoses — the WARN then says the store could not be queried yet instead of blocking. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e71f4696ee |
fix(host/windows): an unlightable sink stops the teardown warn+force loop
A box whose only external display is an off/standby TV warned "no external physical display active after the restore (connected=1)" and forced the EXTEND preset on EVERY teardown — an inert remedy (with one connected display the preset can't relight anything) that reads like a fresh failure in every field log. The backstop now measures its own effect: after a force-EXTEND it re-reads the inventory, and a dark set that stayed dark THROUGH the preset is latched as unlightable — subsequent teardowns log a debug line and leave the topology be. The poisoned-snapshot chain the backstop exists for is not latched away: there the panel CAN light, so the first force succeeds and the latch stays clear. Process-lifetime latch, so a host restart re-probes once. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d014cea489 |
feat(core/abr): a keyframe-begging decoder backs the bitrate off, loss or no loss
The RX-9070 field trace: 14 decode-recovery keyframe requests in 2 s at ~300 Mbps with loss_ppm=0 — and the controller held the rate, because no loss/OWD/latency signal moved. Repeated keyframe asks ARE the decoder saying it cannot keep up; now they are an ABR input. The control task counts outbound CtrlRequest::Keyframe at its send choke point (the one place every emitter funnels through); the pump drains the count per 750 ms report window into on_window. Two asks in a window is ordinary-bad (two-window confirmation, like an OWD rise); four or more is severe (the emitters throttle at 100 ms, so 4+ means the window was spent begging) and backs off ×0.7 after one window. RFI asks are deliberately not counted — they are the routine loss-recovery path and loss_ppm already prices them in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cd6ceb98e3 |
feat(capture/windows): stall lines say whether the content or the display path went silent
A compose-silence hole used to be blamed on the frame-generation path by default — mislabeling benign content pauses (menus, loading, game hitches) as display-path bugs. The corrected discriminator convicts on witnesses: - Microsoft-Windows-DXGI Present/PresentMPO (ids 42/55) rides the host's filtered ETW session — one event per swapchain present, stamped with the presenting pid (named in the stall line). - BltQueueAddEntry/CompleteIndirectPresent (ids 1071/1068) witness frames entering/leaving the virtual display's kernel present queue (the modern IDD path; anatomy proven on-glass via xperf, 2026-07-30). - classify() now reads the window counts: presents flowing while the queue starved = FRAME-GENERATION (the OS dropped composed frames — the real bug class, never yet observed); no presents anywhere = CONTENT-SILENCE (benign for the display path); no working witness = UNATTRIBUTED, never a guess. - DWM_TIMING_INFO.cFrame is demoted to an advisory dwm_frames_frozen= print: on Win11 it is refresh-synthesized and advances without real composes (proven against a kernel trace), so it convicts nothing. - DxgKrnl legacy Present (id 184) is retired — it never fires on the redirected BltQueue path. Stall lines carry etw_presents=/etw_queue_adds= plus named presenters; compiled and validated on-glass (a quit-transition stall correctly labeled CONTENT-SILENCE presents=0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f87c1e6cec |
fix(encode): gate multi-slice frames on the client's decoder — the 0.17.0 Chromecast crash
ci / docs-site (push) Successful in 1m9s
apple / swift (push) Successful in 1m15s
ci / web (push) Successful in 3m16s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 7s
deb / build-publish-client-arm64 (push) Successful in 2m13s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 5s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 7s
ci / rust-arm64 (push) Successful in 3m34s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 13s
deb / build-publish-host (push) Successful in 4m20s
docker / builders-arm64cross (push) Successful in 6s
docker / deploy-docs (push) Successful in 28s
deb / build-publish (push) Successful in 6m17s
arch / build-publish (push) Failing after 7m58s
flatpak / build-publish (push) Successful in 9m6s
ci / rust (push) Successful in 13m37s
release / apple (push) Successful in 16m32s
windows-host / package (push) Successful in 18m21s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 32s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m21s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m44s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m24s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m27s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 2m56s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 3m54s
apple / screenshots (push) Successful in 20m45s
android / android (push) Successful in 5m0s
Field report: since 0.17.0 a stream to a Chromecast with Google TV 4K freezes on the first frame and ~80% of the time crashes + reboots the DEVICE — with both the Punktfunk app and Moonlight, while an Xbox Series S is fine. Root cause: LN1 Phase 3 ( |
||
|
|
ef2bb56251 |
fix(tray): probe /login without redirects, and open the address the probe checked
ci / docs-site (push) Successful in 1m17s
apple / swift (push) Successful in 1m17s
ci / rust-arm64 (push) Successful in 1m48s
ci / web (push) Successful in 1m56s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 8s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 6s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 9s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
deb / build-publish-client-arm64 (push) Successful in 2m6s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
deb / build-publish (push) Successful in 5m3s
docker / builders-arm64cross (push) Successful in 6s
deb / build-publish-host (push) Successful in 4m14s
docker / deploy-docs (push) Successful in 27s
ci / rust (push) Successful in 6m52s
android / android (push) Successful in 7m11s
arch / build-publish (push) Failing after 7m24s
apple / screenshots (push) Successful in 14m27s
windows-host / package (push) Successful in 18m20s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m38s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m10s
Two ways the tray could misreport a healthy console. The probe hit `/`, which is auth-gated and 302s to /login, and ureq follows redirects by default — so TLS, `/`, and a full cold Nitro SSR of /login all had to fit inside one 2s budget, and a console that was merely warming up read as down. It now probes /login with redirects(0) on the agent: one round trip, and a 302 already counted as up. Neither user of that agent wants redirects; the summary is a terminal JSON route. The menu then opened https://localhost:<port> while the poller probed 127.0.0.1. web-run.cmd binds HOST=0.0.0.0 — IPv4 only — and Windows resolves localhost to ::1 first, so the tray could call the console healthy and hand the browser a URL that fails. Both are 127.0.0.1 now, which is what the Linux tray already did, so the menu can no longer disagree with the status printed above it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d9912aa795 |
fix(windows/update): an update puts the status tray back instead of killing it for good
Every update started from the web console killed the tray permanently. Three pieces had to line up, and they did: the installer's StopTrays force-kills every punktfunk-tray.exe (it is one of the files being replaced), its relaunch is a [Run] entry flagged skipifsilent, and the in-console updater spawns the installer with /VERYSILENT. So the tray died on every such update and waited for the next sign-in, because the Run value is a logon trigger. Confirmed on a box whose tray was absent while its Run key, its exe and a session signed in hours before the update were all present. The installer cannot fix this itself. Spawned from the SYSTEM host service, its `runasoriginaluser` resolves to SYSTEM — so relaunching there would place a SYSTEM-owned tray in the user's session, where it would hold the per-session Local\PunktfunkTray mutex and block the real tray at the next sign-in. Strictly worse than the bug. The host does it instead, because only the host has the right token. IntentRecord gains tray_was_running, captured before the installer is spawned (reusing the conflicting-host scan's process snapshot rather than opening a second one), and boot reconciliation reads it off the intent before reconcile consumes it and relaunches through tray::start(). Restored on both terminal outcomes — a rolled-back install killed the tray just as dead as a successful one — but never while an apply is still in flight, where the installer may only kill it again. The field is serde(default), so an intent written by an older host reads as false and behaves exactly as before; covered by a test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c77823d800 |
feat(windows/tray): punktfunk-host tray start|stop|status, so a dead tray is one command away
The status tray is a per-user, per-session GUI process with no recovery path of
its own: the HKLM Run value only fires at sign-in, and nothing in the product
ever restarts one. Anything that kills a tray — an upgrade's StopTrays, a crash —
therefore left the operator without an icon until they signed out and back in,
with no way to ask for it back.
windows/tray.rs becomes the one place that knows tray lifecycle (find, start,
stop, is-running), so the CLI and post-update reconciliation share an
implementation rather than growing two.
start() cannot simply spawn the exe, because the launch crosses a privilege
boundary in one direction only:
- from the host service (SYSTEM) the tray must land in the active console
session under the LOGGED-IN USER's token, which is WTSQueryUserToken +
CreateProcessAsUserW and needs SE_TCB — SYSTEM-only;
- from a shell already in the console session that call fails (an administrator
does not hold SE_TCB either) and a plain spawn is both sufficient and right.
Trying the privileged path and falling back discriminates the two without
inspecting a token, and adds no unsafe to the crate. But a plain fallback from
ssh/RDP would put a tray in a session nobody can see and report success, so on
failure it consults console_session_mismatch() and refuses with an explanation
instead. stop() is graceful first (--quit lets the tray remove its own icon via
NIM_DELETE, as the uninstaller does), then forces any instance in another session.
Verified on Windows: launched from a SYSTEM scheduled task the tray comes up in
the console session owned by the interactive user, not SYSTEM; from an ssh
session (0 -> console 2) it refuses; a second start is a no-op; stop is graceful
and idempotent.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
3bb30cb2f6 |
fix(windows/web-console): a bun exit no longer parks the console until the next sign-in
The PunktfunkWeb task registers RestartCount=10/PT1M, but Task Scheduler honours restart-on-failure only when the action crashes or fails to start — never for a plain non-zero exit. So any bun exit left the console down until the next boot or interactive logon, with the host still streaming perfectly next to it. Seen on glass on a 0.22.3 upgrade: `web setup` started the task one second after the new service came up, bun exited at once (Last Run Result 0xFFFFFFFF), and the console sat dead for hours while its box served clients — which reads to an operator as "the host is gone". web-run.cmd now supervises bun instead of exiting with it: restart on any exit, indefinitely for a console that served a while and then died, but give up after 10 CONSECUTIVE fast exits so a genuinely broken install still surfaces as a failed task. Any run lasting >= 60s resets that counter. It exits 0 rather than respawning when the payload disappears, so an uninstall does not make it spin. Uptime is measured with an unambiguous clock rather than parsed out of %TIME%, which is locale-formatted (12-hour locales append " PM", many others use ',' as the decimal separator) — the box this was found on prints 18:36:28,39. stop_web_console() polls until :47992 is really free instead of sleeping a blind second: both `schtasks /end` and `taskkill /F` are asynchronous, and web_setup starts the new task immediately afterwards, where bun cannot bind a port the corpse still holds. It re-ends the TASK halfway through, not just the listener — with the launcher now supervising, killing bun alone would be undone by its own restart loop. The systemd unit had the same shape of hole: Restart=on-failure leaves a console that exited 0 down. Now Restart=always, which an explicit `systemctl stop` still overrides. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e90c5d5bcd |
fix(gamepad): the output-report ring absorbs haptics-rate writers (PadShm v2.2)
ci / web (push) Successful in 1m1s
windows-drivers / probe-and-proto (push) Successful in 1m3s
ci / docs-site (push) Successful in 1m16s
windows-drivers / driver-build (push) Successful in 2m15s
ci / rust-arm64 (push) Successful in 3m27s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 8s
deb / build-publish-client-arm64 (push) Successful in 2m21s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 9s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 6s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 15s
apple / swift (push) Successful in 4m36s
deb / build-publish-host (push) Successful in 4m2s
docker / builders-arm64cross (push) Successful in 5s
docker / deploy-docs (push) Successful in 29s
arch / build-publish (push) Failing after 6m11s
deb / build-publish (push) Successful in 6m6s
android / android (push) Successful in 8m29s
ci / rust (push) Successful in 10m32s
windows-host / package (push) Successful in 16m18s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 12s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m15s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m42s
apple / screenshots (push) Successful in 20m33s
A field log (2026-07-30) showed a game driving the virtual DualSense's output endpoint at >2 kHz sustained for tens of seconds. The 8-slot ring — sized on the assumption that 2 kHz is double any real HID output rate — overflowed every 4 ms poll, which force-silenced rumble for each storm's whole duration and flooded the log at ~230 WARN lines/s (96 % of the user's 5000-line web-console export, evicting the session history it was needed to diagnose). Three legs, negotiated so every old/new host×driver pairing keeps working: - pf-driver-proto: the ring grows in place 8 -> 56 slots; PadShm becomes exactly one page (4096 B), the hard ceiling that keeps cross-generation section views mappable. A new out_ring_len field carries the driver's side of the length negotiation. Deliberately NOT a GAMEPAD_PROTO_VERSION bump (that fails closed - no pad at all). - pf-gamepad driver: picks its ring length from the host's out_ring_ver stamp (>= 2 + a full-size map -> 56) and echoes it before every ring_head bump (now a Release store), so an Acquire-observing drain always reads the modulo that indexed the slots it copies. - host drain: follows the echo (0 = old driver = 8); on genuine overflow it now salvages the legacy latest-report slot - the freshest coalesced state - instead of total silence, and the per-poll overflow WARN is rate-limited to 1 line/s per pad with a suppressed count. Verified on the Windows CI runner (drivers workspace build + clippy -D warnings against the WDK; 64 pf-inject tests incl. the new negotiation/ salvage/limiter tests; pf-inject clippy -D warnings) and on Linux via the CI docker image (82 pf-inject tests). DriverVer needs no manual bump - the installer stamps a strictly-increasing build timestamp per release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
696386dee7 |
fix(host/windows): clippy — SAFETY comments on the WinTrust FFI, explicit truncate(false), sort_by_key
deb / build-publish-client-arm64 (push) Failing after 4s
ci / docs-site (push) Successful in 1m18s
ci / web (push) Successful in 2m12s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 14s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 13s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 11s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 56s
deb / build-publish-host (push) Successful in 4m28s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 31s
docker / builders-arm64cross (push) Successful in 11s
ci / rust-arm64 (push) Successful in 5m49s
docker / deploy-docs (push) Successful in 31s
android / android (push) Successful in 6m54s
deb / build-publish (push) Successful in 5m0s
apple / swift (push) Successful in 5m6s
ci / rust (push) Successful in 6m13s
arch / build-publish (push) Failing after 7m37s
windows-host / package (push) Successful in 15m8s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 41s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m40s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m39s
apple / screenshots (push) Successful in 21m28s
windows-host.yml's clippy sees what the Linux gate structurally cannot (cfg(windows) code); verified with the same clippy line on the runner this time, not just cargo check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3b1485e2a1 |
fix(plugins/windows): the runner can read its own bundle, so it actually starts
ci / rust (push) Canceled after 27s
ci / rust-arm64 (push) Canceled after 28s
ci / web (push) Canceled after 28s
ci / docs-site (push) Canceled after 28s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 0s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 0s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Canceled after 0s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
apple / swift (push) Successful in 4m45s
android / android (push) Successful in 4m58s
deb / build-publish-host (push) Successful in 4m14s
deb / build-publish (push) Successful in 5m51s
deb / build-publish-client-arm64 (push) Successful in 1m19s
arch / build-publish (push) Failing after 6m54s
windows-host / package (push) Failing after 10m59s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
apple / screenshots (push) Canceled after 14m9s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m17s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 14m33s
Enabling the plugin runner did everything right and then nothing happened.
The task went Ready with LastTaskResult 1 within a second of every start, and
the console kept showing it enabled-but-not-running with nothing to explain
why. Task Scheduler discards the action's output, so the reason never
surfaced anywhere:
error: EPERM reading "C:\Program Files\punktfunk\scripting\runner-cli.js"
This file already knows why. RUNNER_UNIT_DIRS carries the note that bun opens
the files it loads asking for FILE_WRITE_ATTRIBUTES on top of read, so a plain
(RX) grant makes them die with EPERM — found on glass when the plugin and
script directories were fixed. The runner's own entry script was missed. It
lives in {app}\scripting, which carries only Users:(RX), and LocalService
reaches that through Authenticated Users, so bun could never open the one file
it was started to run. The runner has not been able to start since it moved
off SYSTEM, which is a principal that has full control everywhere and so never
met this.
So {app}\scripting now gets the same (OI)(CI)(RX,WA) the unit dirs get, at
enable, revoked at disable like every other grant here. WA moves timestamps
and the read-only bit and cannot touch content, so the three-way split the
module maintains still holds: code read-only, secrets read-only, only
plugin-state writable. A plugin still cannot rewrite the runner.
Measured on glass on .173, same box, same task, only the ACE differing:
without it Ready/lastResult=1 and no runner process; with it Running, a live
runner, and GET /store/runtime answering running:true — which is exactly what
the console reads. Also checked what bun does with the access: nothing. It
opens the entry for write and never writes, and the directory is byte-identical
afterwards.
Windows-only code, so it is rustfmt-clean and type-reviewed but NOT
compile-verified here — the Windows CI job is the gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
4a540bddc8 |
feat(host/deck): one-click source rebuild — the update.sh run as a transient user unit
ci / rust-arm64 (push) Successful in 1m36s
ci / docs-site (push) Successful in 1m28s
ci / web (push) Successful in 2m25s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 9s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 1m13s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
deb / build-publish-client-arm64 (push) Successful in 2m27s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m47s
deb / build-publish-host (push) Successful in 5m53s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 7s
android / android (push) Successful in 7m2s
docker / builders-arm64cross (push) Successful in 11s
arch / build-publish (push) Failing after 7m33s
ci / rust (push) Successful in 7m35s
docker / deploy-docs (push) Successful in 39s
windows-host / package (push) Failing after 11m1s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
deb / build-publish (push) Successful in 6m53s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m4s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m21s
apple / swift (push) Successful in 4m58s
apple / screenshots (push) Successful in 21m32s
The U3.1 leg of planning:host-update-from-web-console.md. The Deck's on-device install gets the Update-now button with no opt-in (user-owned, no root): update.sh --pull runs under systemd-run --user so the script's own restart of punktfunk-host can't kill it mid-build (a child in our cgroup would die with us). Outcome without version equality — which a source rebuild can't promise: a failed build leaves the host alive to report it (unit watched to failure, log attached); a successful one restarts us, and the new source_build intent flag makes intent-present-at-boot itself the success signal (the script only restarts the host after a successful install). The console stops treating a live long build as a timeout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
51d5f6cb29 |
feat(host/windows): boot-loop auto-rollback — the supervisor re-runs the cached previous installer
ci / docs-site (push) Successful in 1m11s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 7s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 6s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 4s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
ci / web (push) Successful in 1m58s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 1m8s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 42s
deb / build-publish-client-arm64 (push) Successful in 3m19s
deb / build-publish-host (push) Successful in 6m17s
ci / rust-arm64 (push) Successful in 6m58s
android / android (push) Successful in 4m51s
docker / builders-arm64cross (push) Successful in 12s
windows-host / package (push) Failing after 5m39s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
docker / deploy-docs (push) Successful in 1m58s
arch / build-publish (push) Failing after 8m14s
apple / swift (push) Successful in 4m59s
apple / screenshots (push) Canceled after 0s
deb / build-publish (push) Successful in 11m51s
ci / rust (push) Canceled after 13m5s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 9m38s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 9m11s
The U3.2 leg of planning:host-update-from-web-console.md. The service worker loop — the one piece that survives an upgrade — detects a fresh update-intent whose target version is the crash-looping child (≥3 rapid restarts, intent <30 min), then exactly once: picks the newest cached non-target installer, requires a valid Authenticode signature, writes a rolled-back result record for the console, deletes the intent, and spawns the installer silently. No cached installer or a failing signature degrade to today's backoff loop with a loud log — never a brick, never an installer loop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
06a249e49f |
feat(host/linux): opt-in one-click updates — the pf-update root helper, group-scoped polkit grant, per-PM legs
The U2 leg of planning:host-update-from-web-console.md. A new dep-free root helper (crates/pf-update) runs the distro package manager against the INSTALLED punktfunk packages — apt (index refresh scoped to our list when present), dnf, rpm-ostree (single-transaction re-resolve, reported staged), sysext (the proven signed-feed updater), pacman only behind the explicit PACMAN_FULL_SYSUPGRADE opt-in — then the run-the-binary gate, then a root-written result record. Zero attacker-influenceable parameters end to end: fixed ExecStart oneshot (punktfunk-update.service), polkit rule scoped to that one unit's start verb for the shipped-EMPTY punktfunk-update group (joining it is the auditable opt-in; every postinst creates it, none populate it). The host starts the unit, interprets the record (staged / nothing-newer-yet / changed), and crosses its own restart on the same intent/reconcile machinery as the Windows leg. Status now reports staged results and the opt-in hint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bb48225414 |
feat(host/windows): one-click update apply — verified installer download, intent/reconcile, POST /update/apply
The U1 leg of planning:host-update-from-web-console.md. The apply request carries no version/url/channel (the zero-parameter invariant): the host installs exactly what its Ed25519-verified manifest announced. Pipeline: staged download (resume + disk preflight) → manifest sha256 → Authenticode (valid signature, untrusted root tolerated while the cert is self-signed; leaf sha256 pinned via the signed manifest, extracted from the SAME WinVerifyTrust state) → intent record → detached CREATE_BREAKAWAY_FROM_JOB spawn of the winget-blessed silent flags. The installer kills this process by design; boot-time reconciliation reports the outcome (update.applied event / a durable failure with the installer log path) across the restart. Session guard (force to override), PUNKTFUNK_UPDATE_APPLY=0 kill switch, single-flight via job + fresh-intent detection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cc01562631 |
feat(host): update check — signed per-channel manifest, install-kind detection, /api/v1/update surface
The U0 leg of planning:host-update-from-web-console.md: a signed update manifest (Ed25519, keys pinned in the binary via the plugin-store verify path, serial floor persisted against rollback, channel-bound, 45-day stale hint) fetched lazily behind GET /update/status + rate-limited POST /update/check, admin lane only (plugin lane whole-prefix denied, absent from the cert allowlist). Install kind + channel come from root-owned facts; deb/rpm/pacman builds now stamp /usr/share/punktfunk/install-kind. Emits update.available once per discovered version. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
940bd0b7ec |
feat(core/abr): the startup capacity probe's target is configurable
ci / docs-site (push) Successful in 1m20s
ci / web (push) Successful in 1m31s
ci / rust-arm64 (push) Successful in 1m44s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 14s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 15s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 13s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 13s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 30s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 30s
apple / swift (push) Successful in 4m43s
android / android (push) Successful in 6m18s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 15s
deb / build-publish-host (push) Successful in 6m14s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 13s
deb / build-publish (push) Successful in 5m16s
docker / builders-arm64cross (push) Successful in 12s
docker / deploy-docs (push) Successful in 39s
arch / build-publish (push) Successful in 9m18s
apple / screenshots (push) Canceled after 4m51s
ci / rust (push) Successful in 9m43s
deb / build-publish-client-arm64 (push) Canceled after 8m52s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 8m7s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 8m5s
windows-host / package (push) Canceled after 10m19s
windows-host / winget-source (push) Canceled after 0s
flatpak / build-publish (push) Successful in 7m26s
release / apple (push) Canceled after 9m48s
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 3m23s
The Automatic startup probe bursts at a fixed 2 Gbps two seconds into every session, deliberately far above any plausible link so the burst measures the link rather than itself. On links the burst DISTURBS, that backfires: on an LG G5 (webOS 10.3), three back-to-back connects to the same Gamescope host split two ways — the two where the probe finished in ~1-2 s had video within 2-4 s, while the one that hit the 6 s timeout showed no video for 14 s. Even a "successful" probe on that link reported send_dropped=20211. The webOS client already caps its own speed test at 320 Mbps because an unbounded firehose starves a 2-3 core TV; core then bursts the same hardware at 2 Gbps. PUNKTFUNK_ABR_PROBE_KBPS now sets that target, so an embedder that caps its own speed test can cap ours to match. Unset, zero, or unparseable keeps the 2 Gbps default, so every existing session behaves exactly as before. The existing opt-out (PUNKTFUNK_ABR_PROBE=0) is no substitute: it leaves the climb ceiling pinned at the negotiated ~20 Mbps. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
553676282a |
feat(windows): the brand reads Punktfunk on every device Windows shows
deb / build-publish-client-arm64 (push) Failing after 4s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 13s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 11s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
ci / web (push) Successful in 1m53s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 13s
ci / docs-site (push) Successful in 2m3s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 15s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 30s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m24s
windows-drivers / driver-build (push) Successful in 2m30s
docker / builders-arm64cross (push) Successful in 6s
docker / deploy-docs (push) Successful in 29s
android / android (push) Successful in 5m7s
apple / swift (push) Canceled after 4m50s
apple / screenshots (push) Canceled after 0s
deb / build-publish (push) Successful in 4m42s
arch / build-publish (push) Canceled after 5m25s
ci / rust (push) Canceled after 5m26s
ci / rust-arm64 (push) Canceled after 5m31s
deb / build-publish-host (push) Canceled after 5m19s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 3m30s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 3m26s
windows-host / package (push) Canceled after 2m29s
windows-host / winget-source (push) Canceled after 0s
windows-drivers / probe-and-proto (push) Successful in 37s
Device Manager, the firewall list and the monitor name all said "punktfunk". The brand
is Punktfunk.
Renamed: the [Strings] blocks of all four driver INFs (device descriptions, install
disks, provider, manufacturer), the `description` on every SwDeviceProfile the host
creates, pf-mouse's HID manufacturer + product strings, pf-vdisplay's IddCx endpoint
friendly + manufacturer names, the EDID 0xFC display-name descriptor — so Windows now
shows `Generic Monitor (Punktfunk)` — and the netsh firewall rule names.
The EDID edit is a single byte (0x70 -> 0x50) and needs no hand-patched checksum:
Edid::generate_with already recomputes both block checksums after patching the serial.
Deliberately left lowercase, because these are IDENTITIES rather than display names and
renaming them would orphan installed state:
* the SwDeviceCreate enumerator `w!("punktfunk")` — it IS the SWD\PUNKTFUNK\... path
every pad instance id is built from
* pf-paths' `join("punktfunk")` — C:\ProgramData\punktfunk
* the CN=punktfunk-driver cert subject, which purge_driver_certs and both driver build
scripts match by string
* install.rs' `lo.contains("punktfunk virtual display")` probes, whose haystack is
to_ascii_lowercase()d, so they already match the capitalised name
Nothing is orphaned by the renames that DID happen either: netsh rule names,
Get-NetFirewallRule -DisplayName and PowerShell's -match are all case-insensitive, so
the firewall delete paths and reset-pf-vdisplay.ps1's -AdapterName / -GhostMatch
defaults still reap what every release up to 0.22.1 created.
Cosmetic, with two consequences worth knowing: it takes a driver rebuild + re-sign to
appear at all, and an existing devnode keeps its cached FriendlyName until it is
recreated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
02e9cc4691 |
fix(host/windows): a virtual DualSense binds our driver again, not Microsoft's
0.22.0 and 0.22.1 hand every default-configured Windows host a controller no game can
see. `GamepadPref::Auto` resolves to DualSense, so this is the pad almost everyone
gets — which is why it reads as "controllers are broken" rather than as one identity
being broken.
The pf-dualsense -> pf-gamepad PACKAGE rename (
|
||
|
|
1f59498c5c |
fix(host/linux): a no-channel session composites the metadata cursor — Mutter never embeds on a virtual stream
ci / web (push) Successful in 3m26s
ci / docs-site (push) Successful in 3m32s
arch / build-publish (push) Failing after 4m29s
apple / swift (push) Successful in 4m38s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 5s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 5s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 4s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 5s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
ci / rust-arm64 (push) Failing after 4m50s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 12s
deb / build-publish (push) Failing after 5m6s
ci / rust (push) Failing after 5m47s
docker / builders-arm64cross (push) Successful in 6s
docker / deploy-docs (push) Successful in 34s
android / android (push) Canceled after 8m16s
apple / screenshots (push) Canceled after 3m46s
deb / build-publish-host (push) Canceled after 4m15s
deb / build-publish-client-arm64 (push) Canceled after 4m7s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 3m6s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 2m58s
windows-host / winget-source (push) Canceled after 0s
windows-host / package (push) Canceled after 8m21s
The capture-latched client (console.rs latched_mouse) never advertises CLIENT_CAP_CURSOR, so its session resolved cursor_blend=false and asked Mutter to EMBED the pointer. On a Mutter virtual stream that is a fiction: since Mutter 48 (7ff5334a, hw-cursor inhibition removed) the software cursor overlay is suppressed stage-globally whenever any physical head realizes a HW cursor — dmabuf-recorded frames blit the view without a pointer, and cursor-only motion schedules no re-record either (mutter#4939). Probed on-glass on Mutter 50.3: embedded + relative motion = frozen frame counter; SPA_META_Cursor positions kept flowing in the same setup. So the no-channel session now takes the path that was verified end to end: cursor-as-metadata + the host composites, permanently — the same arm a channel session lands in after its capture-model flip. Embedded remains only the can't-blend fallback (libav VAAPI/NVENC, software). - session_plan::cursor_blend_for grows the no-channel arm (codec + depth in, the same CUDA-payload prediction handshake makes); gamescope excluded so patch-2+ keeps its native-NV12 zero-copy shape - the encode loop's composite refresh + one-shot breadcrumbs now cover the no-channel session; the park schedule keeps retrying while its composite is starved (relative-only clients cannot park themselves) - the compositor retarget re-applies set_hw_cursor — the rebuilt display used to come up EMBEDDED even for channel sessions - the GameStream virtual source takes the same rule (it never has a channel); its stream_body blend flag mirrors the request - punktfunk-probe grows --cursor-nochannel (the latched-capture client, headless); cursor-probe grows --dump (PPM frames + a content-change counter, the pixel evidence the embedded A/B lacked) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2e6cd0e235 |
feat(capture): stall attribution phases A.2+A.3 — micro-probes and DxgKrnl ETW name the class
ci / web (push) Successful in 2m30s
ci / docs-site (push) Successful in 2m49s
apple / swift (push) Successful in 4m36s
ci / rust-arm64 (push) Successful in 10m10s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 6s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 5s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 4s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 5s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 43s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
android / android (push) Successful in 12m44s
deb / build-publish (push) Successful in 11m34s
docker / builders-arm64cross (push) Successful in 23s
windows-drivers / probe-and-proto (push) Successful in 29s
docker / deploy-docs (push) Successful in 30s
arch / build-publish (push) Successful in 15m48s
ci / rust (push) Successful in 16m21s
windows-drivers / driver-build (push) Successful in 1m46s
deb / build-publish-client-arm64 (push) Successful in 9m22s
deb / build-publish-host (push) Successful in 19m7s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 12m34s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 11m40s
apple / screenshots (push) Successful in 20m22s
windows-host / package (push) Successful in 14m39s
windows-host / winget-source (push) Skipped
Phase A.2: a refcounted micro-probe engine (fence round-trip per adapter, DwmGetCompositionTimingInfo tick, watchdogged DwmFlush, Level-Zero D3DKMTGetScanLine, CPU jitter sentinel) samples continuously on detached sacrificial threads; each stall report reads the window back and the verdict matrix folds it with the driver telemetry into a named class: ours-worker / ours-delivery / CLASS-1 adapter freeze / CLASS-2 compositor blocked / frame-generation / unattributed. The metronomic WARN carries the per-class session tally. Phase A.3: an event-id-filtered real-time ETW session on Microsoft-Windows-DxgKrnl (QueryChildStatus 150/151, SetPowerState 154/155, IndicateChildStatus 272, SetTimingsFromVidPn 430, DisplayDetectControl 1096/1097) rides every stall line as a DDI bracket summary — naming the servicing call and its duration instead of 'below Windows'. Degrades to etw=unavailable without admin; probes degrade per-leg (absence is stated, never guessed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b2168dae6a |
feat(vdisplay): stall attribution v1 — the driver testifies which leg lost the frames
Every IDD-push capture stall now carries a VERDICT instead of a hypothesis list. The shared ring header grows a v2 telemetry tail (drain-loop heartbeat QPC, last-acquire QPC, full-width offered counter) the driver stamps on every drain pass; the host samples it between fresh frames and attributes each stall: worker-stalled (our thread starved) / compose-silence (DWM composed nothing — the disturbance is below capture) / delivery-leg (frames existed, our publish/ring/consume lost them). The metronomic WARN prints the running tally, so one pasted log line settles the Branch-1/Branch-2 fork of the vdisplay-disturbance-immunity program per session, per box. Both directions stay version-safe: the tail is gated on the HOST-stamped header version (a v2 driver never writes past a v1 host's 64-byte layout — it maps the whole section instead of a fixed 88 bytes), and a v2 host reads a zero heartbeat as pre-telemetry driver, no verdict. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a420855b5a |
feat(tray): the Windows tray dresses for Windows 11 and announces connects
The menu was a stock Win32 popup: light-mode on a dark taskbar,
DPI-virtualized (blurry on every scaled laptop), no icons. Now the
process opts into the system dark mode via the uxtheme ordinals
(135/136 - the same undocumented calls Explorer/PowerToys/Notepad++
make; menus never got a documented opt-in, and there is no WinUI tray
API to move to), a PerMonitorV2 manifest makes menu and icon crisp, the
multi-size .ico serves the DPI-correct frame, and items carry Segoe
Fluent glyph bitmaps - with the UAC shield on the elevated service
actions, per Explorer's convention. Rounded corners come free on
Windows 11. Everything degrades gracefully: missing ordinals mean the
classic light menu, a missing icon font means no glyphs.
New: a connect toast on the idle-to-streaming edge - title "<device>
connected" (from the summary's new client_name), body the mode
("Streaming 2560x1440 @ 120 fps") - via NIF_INFO, which Windows 11
renders as a native toast. The tray tags itself with the AUMID
unom.punktfunk.tray and the installer registers it under
Classes\AppUserModelId (DisplayName "Punktfunk" + the brand icon), so
the toast is attributed to Punktfunk with the logo. Unregistered dev
runs degrade to generic attribution, older hosts to a nameless title;
a tray started mid-session never fires a stale toast.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
9b2404a580 |
feat(core): every connect introduces the device by name
The wire always had Hello.name and the host always honored it - but the connect path hardcoded None (only the PIN-pairing ceremony sent a name), so every no-PIN "request access" knock surfaced as the fingerprint placeholder "device abcd1234", and approving one without retyping a name persisted that placeholder into the trust store forever. NativeClient::connect now takes the device name. The session workers and the probe connects pass trust::device_name() (the hostname), the C ABI defaults to the same without a signature change (an ex10 variant can make it explicit if an embedder wants a custom label), and Android threads Build.MODEL through nativeConnect - the same convention its pairing dialogs already use for nativePair. The host, in turn, resolves the streaming client's display name (trust store first, so an approval-time rename wins; else the sanitized Hello name) and exposes it as client_name in GET /api/v1/local/summary for the tray's connect toast - a deliberate, documented loosening of that route's "no device names" contract, in the local user's favor: it tells them who is on their machine. A paired-but-idle device's name still never appears, which the mgmt tests now pin explicitly. openapi.json, its docs-site copy, and the SDK bindings regenerate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
35c61fee64 |
chore(client): the windows-rs pin moves a month forward, onto the SDK-metadata bindings
The July 2026 windows-rs brings a reconciler keyed-child-order fix (#4728), widget validation (#4727), a DPI collision fix (#4751), icon elements (#4736), multi-window support (#4730) and scroll virtualization (#4710) — the re-render fixes the Windows client has been working around at the architecture level. All three pinned deps (windows-reactor, windows, windows-reactor-setup) move together so windows-core stays unified across the swap-chain hand-off, and pf-client-core moves with them. The bulk of the diff is #4689: windows/windows-sys now generate straight from the Windows SDK, so the `Win32_*` namespace features became one feature per SDK header (winuser, dxgi, d3d11, …), the PascalCase namespace modules became header-named modules, struct-returning COM methods take explicit out-params and return HRESULT, Win32 functions return their raw BOOL/HANDLE instead of Result, and flag constants are plain integers. Both crates' Win32 code is rewritten to that shape; behaviour is unchanged on every path. Riding along, all already stale before the bump: the README and the three Windows workflows stop claiming windows-reactor's build.rs needs CARGO_WORKSPACE_DIR (that build.rs no longer exists — staging moved to windows-reactor-setup via OUT_DIR); the README layout section stops describing modules that moved into the session binary long ago and gains the manual smoke checklist; the notices generator learns the SPDX for crates that ship license files without a `license` field, which turns windows-reactor-setup's UNKNOWN into MIT OR Apache-2.0; and the crate records its real rust-version (1.96) instead of inheriting the workspace's 1.82. Verified: cargo check/clippy/fmt clean on punktfunk-client-windows, pf-client-core and punktfunk-client-session; both bins build; --discover finds the LAN hosts; the GUI shell comes up (WinAppSDK bootstrap intact under the new reactor-setup). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ddd0e5cc5d |
fix(vdisplay/kwin): primary is a PRIORITY, and the log now reads the answer back
audit / bun-audit (plugin-kit) (push) Successful in 19s
audit / bun-audit (sdk) (push) Successful in 17s
audit / pnpm-audit (push) Successful in 10s
audit / docs-site-audit (push) Successful in 28s
audit / bun-audit (web) (push) Successful in 30s
ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 2m26s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Failing after 3m35s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Failing after 2m50s
ci / bench (push) Successful in 6m47s
deb / build-publish (push) Failing after 6m36s
audit / license-gate (push) Successful in 7m54s
decky / build-publish (push) Successful in 32s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 55s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 46s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 28s
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 17s
ci / rust-arm64 (push) Successful in 10m48s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m55s
apple / swift (push) Successful in 4m50s
android / android (push) Successful in 12m33s
deb / build-publish-client-arm64 (push) Failing after 5m44s
deb / build-publish-host (push) Failing after 10m5s
ci / rust (push) Failing after 13m19s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 5m4s
docker / deploy-docs (push) Successful in 29s
docker / build-push-arm64cross (push) Successful in 37s
audit / cargo-audit (push) Successful in 2m46s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m2s
flatpak / build-publish (push) Failing after 8m44s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 12m5s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 12m18s
arch / build-publish (push) Successful in 15m43s
apple / screenshots (push) Canceled after 0s
release / apple (push) Canceled after 18m42s
windows-host / package (push) Canceled after 13m31s
windows-host / winget-source (push) Canceled after 0s
A field report (CachyOS, KWin 6.7.3, Plasma): topology primary streams the new virtual display but the desktop stays on the physical — while the host logs primary_taken=true on every apply. Both halves were real: * KWin's handler for kde_output_configuration_v2.set_primary_output is literally '// intentionally ignored' (verified in the 6.7.3 source). The output order is driven by per-output set_priority (management >= 3; we bind up to v22 and never called it). Exclusive only ever LOOKED right because disabling every other output leaves KWin nothing else to promote. Now: ours gets priority 1 and every other enabled output is renumbered uniquely behind it, exactly what kscreen-doctor does — the reporter proved that path applies and sticks on their box. set_primary_output is still sent for pre-v3 compositors. * primary_taken echoed the REQUEST. The log now waits one sync barrier for KWin's post-apply priority events and reports primary_verified from the read-back — a request the compositor ignores can never ship a green log again; the mismatch is a WARN naming the symptom. First-slot-wins refinement the working priorities made necessary: a SAME-NAMED output is this slot's own predecessor mid-supersede (mode switches create the replacement before dropping the old), not a sibling whose primary must be respected — deferring to it handed primary to whatever KWin promoted when the predecessor vanished a moment later. On-glass (nested KWin 6.7.3): fresh create, high-refresh custom-mode create, and the mode-switch supersede all read back verified=true; exclusive still disables + restores the other output in-process. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
751fbd6c47 |
fix(host/linux): park the seat pointer on the streamed surface — a capture-mode client cannot
ROOT CAUSE of the GNOME capture-mode cursorless stream (and the same latent hole on KWin, whose screencast also gates cursor data on includesCursor): a virtual output is created fresh per session while the seat pointer stays wherever it was — usually a physical monitor. A capture-model (pointer-lock) client sends only RELATIVE deltas, so nothing ever moves the pointer INTO the streamed output: input lands on the wrong monitor, Mutter suppresses SPA_META_Cursor entirely (should_cursor_metadata_be_set: visible AND in-stream), and both the embedded and the encoder-blend composite models stream cursorless. The stream loop now parks the pointer at the streamed surface's centre through the session's own input pipeline (capability routing, region ladder, anchor — the path client events take): armed per (re)built display and by the capture-model flip, retried on a schedule because the first park of a session can land on a still-cold EIS connection (devices not resumed — the injector drops it; observed on-glass), and kept trying while a capture-model session still has no live overlay. A desktop-model client overrides the park with its first absolute move. Verified end-to-end on GNOME 50.3/Mutter (RTX 5070 Ti): the reference client in capture mode now shows the host-composited cursor moving in the decoded H.265 dump, and the PyroWave session hands the overlay to the wavelet CSC blend. One-shot per-session/per-stream breadcrumbs (first meta region, first bitmap, first overlay handed to the blend / still-cursorless) make the next field triage a grep instead of a rebuild. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
39aa0c57ce |
fix(inject/libei): a scaled output's region is the streamed mode in logical pixels — match it
A display scale s shrinks an output's EI region to logical pixels (Mutter advertises 853x533 for a 1280x800 output at 1.5), so the exact-size rung missed every scaled output and absolute input fell through to regions.first() — the wrong monitor whenever another region sorts first (on-glass: a lingering sibling virtual display). New rung between exact and first: one consistent scale factor (1..=4, fractional included) must map the region onto the mode on both axes, with per-axis rounding slack. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
944c03dd32 |
feat(client): the desktop clients wear the host's OS mark
The client half of the host's new `os=` advert, shared once in pf-client-core: `sanitize_os` (mDNS is unauthenticated input — lowercase `[a-z0-9._-]` tokens, capped) and `os_icon_tokens`, the most-specific-first walk with the brand aliases (`macos`→apple, `steamos`→steam) every platform resolves through. `DiscoveredHost` carries the chain, `KnownHost` persists it (`serde(default)`, elided when empty — older stores load unchanged and older clients read back exactly what they wrote), `upsert` moves it only when carried, and `learn_os` mirrors `learn_mac` — no-op, no disk write when unchanged — so the mark survives the host going to sleep. GTK shells: the card's status row leads with a recolorable symbolic glyph. That needed real embedded assets — the shells had none — so `data/` gains the ten `pf-os-*-symbolic` SVGs compiled into a gresource (new build.rs, glib-build-tools) and registered on the icon theme at startup; the Adwaita theme then tints them like every other status glyph. WinUI shell: reactor renders raster-from-URI only, so the embedded mid-gray PNGs (legible on both themes) materialize once into %LOCALAPPDATA%\punktfunk\os-icons\ — the library's poster-art pattern — and the tile's status row leads with a 16px image. The couch UI plumbs `HostRow.os` (live advert preferred, else the store) for a Skia glyph that is a declared follow-up; `--list-hosts` / `hosts --json` emit the stored chain so the Decky plugin can read it. A host that advertises no `os` renders everywhere exactly as it did before the field existed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4c2a6dd091 |
feat(host,api): the host knows what OS it runs, and says so
Clients want to show which OS/distro a host runs. The host now detects it once per process (new osinfo module: compile-time on Windows/macOS, os-release on Linux) and advertises an icon-friendly specificity chain, generic → specific — `windows`, `macos`, `linux[/<family>][/<id>]`, e.g. `linux/fedora/bazzite`, `linux/arch/steamos`. A client walks the chain most-specific-first and shows the first token it has art for, so an unknown distro degrades to its family's mark and finally to plain Tux — the host emits the full chain precisely so clients need zero distro→parent knowledge. The middle token is the first recognized ID_LIKE ancestor (the spec orders them most-similar-first); the leaf is ID verbatim, sanitized to TXT-safe `[a-z0-9._-]` because it feeds a DNS record. Two carriers, both additive: a new advisory mDNS `os=` TXT key (same trust posture as `mac` — unauthenticated, a wrong value only draws a wrong icon), and `HostInfo.os` + `HostInfo.os_name` on the mgmt API (`os_name` is the os-release PRETTY_NAME, REST-only so the TXT stays small). GameStream serverinfo and the QUIC Welcome are untouched. api/openapi.json regenerated (drift test green on Linux) and the TS SDK gen refreshed from it — which also catches the committed gen up with earlier spec changes it had missed (monitors, native_paired_clients, encoder_backend). assets/os-icons/ gains the ten master SVG marks every client derives its per-platform art from (Font Awesome Free brands CC BY 4.0 + Simple Icons CC0 — provenance in its README), with attribution folded into THIRD-PARTY-NOTICES.txt via the generator (regenerating also catches the stale crate manifest up with the current lockfile). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b0ea1e6b51 |
fix(client/linux): the override marker appears on touch, profiles get a colour, and 4:4:4 gets a switch
Three things found by actually driving the client. **The marker didn't appear until you reopened the dialog.** It was rendered once, at build time, from the stored overlay — so changing a setting inside a profile looked like it did nothing. The design says touching a control creates the override and the marker appears immediately, and it has to: a user who changes a row and sees no acknowledgement has no reason to believe it took. Every profileable row now builds its dot and reset hidden, and the same handler that records the touch reveals them. Resetting a row touched in the same sitting also un-touches it, so the commit can't re-write what the reset just removed. **Profiles had no colour.** `accent` has been in the schema since P0 and nothing could set it, which left every chip the same grey — and telling profiles apart at a glance across a grid is the entire reason chips exist. Creating a profile now picks a colour in the same breath as its name (hunting for it afterwards is what leaves them all grey), an existing profile has a Colour row, and host-card chips are tinted with it. A palette of eight rather than a free picker: legibility across light and dark is the job, and the schema still accepts any `#RRGGBB` a hand-edit or a future picker writes. Anything that isn't `#RRGGBB` is refused rather than interpolated into CSS, and each distinct colour registers one display-wide rule (per-widget providers are gone since GTK 4.10). **4:4:4 had no switch anywhere but Apple.** `VIDEO_CAP_444` has been on the wire for a while with only `punktfunk-probe`'s env var to set it. It is now a setting — and a profileable one, which is the point: full chroma is what makes small text and thin UI lines crisp, so a "Work" profile wants it where "Game" usually doesn't. The host still gates it on its own policy, HEVC, and a GPU that can actually encode it; advertising only says "I can decode this and I want it". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9243a770bc |
fix(host/encode): an Automatic bitrate follows the pixels actually encoded
ci / web (push) Successful in 53s
ci / docs-site (push) Successful in 2m10s
ci / bench (push) Successful in 7m34s
deb / build-publish (push) Failing after 6m38s
decky / build-publish (push) Successful in 40s
apple / swift (push) Successful in 5m5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 56s
ci / rust-arm64 (push) Failing after 9m29s
ci / rust (push) Failing after 9m31s
deb / build-publish-host (push) Failing after 7m15s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 30s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 29s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 30s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 17s
android / android (push) Successful in 12m31s
apple / screenshots (push) Canceled after 4m1s
arch / build-publish (push) Canceled after 13m14s
deb / build-publish-client-arm64 (push) Canceled after 5m40s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 3m48s
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 3m39s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 3m39s
windows-host / package (push) Successful in 16m35s
windows-host / winget-source (push) Skipped
`build_pipeline` opens the encoder at the DELIVERED frame size but was handed a bitrate resolved from the NEGOTIATED mode. Those agree for a virtual display, which is created at the mode that was asked for. They do not agree for a monitor mirror: `MirrorDisplay::create` ignores the requested mode by design (a physical head runs at the mode its owner set), so a client asking for 1080p and mirroring a 4K panel encoded four times the pixels at the 1080p rate — a quarter of the bits per pixel the codec was sized for, which arrives as a soft, stuttering picture rather than as the mismatch it actually is. This is the reported "laggy mirror". Re-resolve for what will really be encoded, and thread the applied rate back out so the session adopts it: `bitrate_kbps` is what the ABR controller climbs from, what the console samples, and what a `SetBitrate` ack is measured against, so letting it disagree with the live encoder makes each of those reason about a stream that does not exist. The mid-stream rebuild paths (mode switch, compositor switch, capture-loss) adopt it too — each can land on a different source than it left. Automatic only. An explicit client rate is the operator's statement about their own link and is never second-guessed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
664c26cb2b |
fix(vdisplay): a pin with no heads to mirror must not refuse the session
The streamed-screen pin is host-wide and persisted, but the session it applies to is not. A box that boots between a desktop (heads to mirror) and a nested or headless Game Mode (none) carried the pin into a session where `monitors::resolve` could only fail — and since `vdisplay::open` is the one place every session opens a display, that failure was a host refusing to stream at all rather than one streaming the normal way. Pinning a monitor in Desktop mode bricked Game Mode. A compositor reporting NO heads whatsoever now degrades to the virtual-display path with the reason logged. Narrow on purpose: a pin that misses among heads that DO exist stays the hard error §5.2 makes it — showing someone a different screen than they asked for is the failure worth refusing over, and "there are no screens here" is not that. An enumeration ERROR also stays on the mirror path, so the session fails with the real reason instead of quietly ignoring the operator. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
028462ef6d |
feat(vdisplay/gamescope): a Game Mode session drives a real head — list it, and mirror it
The per-monitor capture feature shipped with gamescope written off as "nested, therefore no physical heads of its own", and `monitors::list` returned a hard-coded empty Vec for it. That premise holds for a gamescope nested in another compositor and for the headless ones this crate spawns — but not for the deployment people actually stream from: a Bazzite/SteamOS Game Mode session is the DRM master and drives the TV directly. On such a box the console's picker was permanently empty, on exactly the hardware the feature was asked for. `gamescope/heads.rs` answers the narrow question instead — is this gamescope on the DRM backend, and which connector is it lighting? — from `/proc/<pid>/cmdline` (the backend flag and `--prefer-output`; gamescope publishes no protocol that reports its own output) and `/sys/class/drm` (what is plugged in, plus each connector's EDID for the picker label and the panel's preferred timing). The nested/headless shapes still answer an empty list, which is what they always meant. Mirroring needs no new capture code: gamescope composites its one head into a PipeWire node it already publishes, so the mirror arm is an ATTACH to that node — the gaming session is not stopped, relaunched or re-moded, which is the whole difference between "stream the panel I am looking at" and the MANAGED takeover that deliberately blanks it. Refresh comes from the EDID and never from `--nested-refresh`: that flag is the rate gamescope composites the nested game at, and reading it as the connector's rate would let a session launched at 60 cap a 120 Hz TV for the whole stream. Verified against home-bazzite-1's live EDID: manufacturer `1e6d` -> GSM, name descriptor -> "LG TV SSCR2", preferred timing 594 MHz / 4400x2250 -> 3840x2160@60. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a49858f194 |
feat(packaging/windows): give the drivers one publisher identity, and give the trust back
The drivers had no cryptographic identity at all. Every build minted a fresh `CN=punktfunk-driver` cert (build-pf-vdisplay.ps1, build-gamepad-drivers.ps1), and install.rs `trust_cert` adds whatever .cer it finds in the unpacked bundle to machine Root AND TrustedPublisher. So the signature vouched for nothing an attacker couldn't restage — replace the bundle, ship your own cert beside your own driver, install proceeds identically. It was ceremony to make PnP install quietly. Worse, it leaked. `trust_cert` runs once per driver (install.rs:102 and :155), so every upgrade added TWO more self-signed root CAs under the same name, and nothing ever removed them: uninstalling punktfunk left trust behind that the user had no reason to keep granting. So: both build scripts now take a stable cert via DRIVER_CERT_PFX_B64 (they already read the env var — windows-host.yml just never passed it) and fail closed on a v* tag, same rule as the host and MSIX packers. `driver uninstall` purges every `CN=punktfunk-driver` cert from both stores, and `driver install` purges before adding, so an upgrade also collects the historical pile instead of adding to it. Purge-before-add lives ONLY on the pf-vdisplay install path, not the gamepad one. The installer runs vdisplay first and gamepad second; purging in both would have the gamepad leg delete the cert the vdisplay leg just added whenever the two bundles carry different certs — which is exactly what canary's per-build fallback produces. Purging by subject rather than thumbprint is deliberate too: it is what lets one install clean up certs from builds that no longer exist anywhere, and it needs no parsing of certutil's localized output (this module exists because locale-parsed PowerShell broke the driver install on a German box). This does NOT make the driver download authenticated — a self-signed leaf is its own root, so the installer must trust it for the driver to install at all. What it buys is a fingerprint we can publish out-of-band so a substituted driver is detectable, an allowlistable publisher, continuity across releases, and no root accumulation. Attestation signing remains the real fix; documented as such. Documented the key-custody trade honestly in packaging/windows/README.md: a stable key trusted as a machine root on every install is worth stealing in a way a throwaway never was, with no revocation path. CI secret only. ⚠️ The secrets must exist before the next v* tag or the release will fail — that is the guard working, and the generation runbook is in that README. The fingerprint line there is a placeholder until the key is generated. Verified: punktfunk-host compiles clean on the windows-amd64 runner with this install.rs (exit 0, no warnings); both build scripts parse; cargo fmt clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bcfb833ff7 |
fix(abi): the panic boundary was documented as universal and wasn't
`abi.rs`'s header states "panics never cross the boundary: every entry point is wrapped in `catch_unwind`". Of its 78 `extern "C"` entry points, 16 were not. Ten of those are fine and were always fine — `punktfunk_abi_version` returns a constant, and the nine `punktfunk_connect*` shims forward every argument unchanged into a guarded implementation — but five ran real code bare: `session_free`, `connection_close`, `connection_disconnect_quit`, `reanchor_gate_free`, `reanchor_gate_arm`. The three `*_free`/`close` ones are the point. They run `Drop` for an entire `Session` or `Connection` — transports, threads, mutexes — and a `Drop` impl that unwraps a poisoned lock panics. Since Rust 1.81 that unwind is a hard abort rather than undefined behaviour, so this is not a soundness hole; it is worse-behaved than it looks. Aborting the CALLER'S process because one of our teardown paths hit a poisoned mutex is not an acceptable failure mode for a library, and it would present as "the app died in punktfunk_session_free" with no Rust backtrace to explain it. Adds `guard_void`, the sibling of `guard` for entry points with no status to report through: it catches, logs, and returns — right for teardown, where the object is going away regardless. The five are wrapped in it. The header now says what is actually true, including WHICH entry points are deliberately bare and why, so the next reader can check the claim instead of trusting it. That is the same failure this sweep found in `MappedView`'s `Sync` proof: a stated invariant that a reviewer would rely on and that had quietly stopped holding. Verified: Linux .21 fmt + both CI clippy steps rc=0, and the C ABI harness — which exercises the `*_free`/`close` paths it touches — still passes, 4 frames byte-exact through lossy loopback. |
||
|
|
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. |