6b3e793b394ef3e1c53a4eaaa78e4732d95da4b7
3231
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6b3e793b39 |
fix(gamestream): bind the UDP/ENet media plane to the launch owner
security-review 2026-08-15 findings 1, 2, 13. The Moonlight-compat plane bound its UDP video/audio endpoints to the first datagram from anyone and let any ENet peer keep a connection (pinning per-peer reassembly memory) — the peer_ip the RTSP/launch planes already enforce was never threaded to the media/control sockets. - stream.rs/audio.rs: the video/audio endpoint learn now discards datagrams whose source IP is not the launch owner's until the 10s budget is spent, so an off-path LAN peer can no longer win the endpoint race and be handed the (plaintext) video stream. - control.rs: an OwnerFilteredSocket drops non-owner datagrams before ENet allocates any per-peer state (closes the ~32 MiB x peer_limit pin and the source-spoof injection variant), and the Event::Receive arm now honors only the tracked session peer's input as defense-in-depth. GameStream is runtime opt-in and off in the shipped unit, so this is deferrable but the code's own comments claimed a peer bind already protected these paths. |
||
|
|
00f9c1f4d3 |
Merge pull request '0.29.0' (#242) from worktree-release-0290 into main
audit / bun-audit (sdk) (push) Successful in 26s
audit / cargo-audit (push) Successful in 38s
audit / pnpm-audit (push) Successful in 12s
audit / bun-audit (plugin-kit) (push) Successful in 1m40s
audit / docs-site-audit (push) Successful in 1m39s
audit / bun-audit (web) (push) Successful in 1m39s
audit / license-gate (push) Successful in 4m34s
audit / miri (push) Successful in 5m4s
ci / web (push) Successful in 1m2s
ci / bun-nix (push) Successful in 16s
ci / docs-site (push) Successful in 1m13s
audit / c-abi-asan (push) Successful in 11m33s
ci / rust-arm64 (push) Successful in 12m19s
nix / flake (push) Successful in 15m45s
ci / rust (push) Successful in 31m23s
deb / build-publish-gamescope (push) Successful in 36s
decky / build-publish (push) Successful in 34s
deb / build-publish-client-arm64 (push) Successful in 1m19s
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 8s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 8s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / builders (ci/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (push) Successful in 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
apple / swift (push) Successful in 2m12s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 55s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m20s
deb / build-publish-host (push) Successful in 4m13s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 5m26s
linux-client-screenshots / screenshots (push) Successful in 3m14s
android-screenshots / screenshots (push) Successful in 7m2s
sbom / sbom (push) Successful in 42s
deb / build-publish (push) Successful in 9m22s
docker / builders-arm64cross (push) Successful in 23s
docker / deploy-docs (push) Successful in 59s
arch / build-publish (push) Successful in 12m48s
web-screenshots / screenshots (push) Successful in 6m8s
android / android (push) Successful in 14m5s
flatpak / build-publish (push) Successful in 11m30s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 9m5s
apple / distribute (push) Successful in 13m54s
deb / smoke-install (push) Successful in 6m26s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m33s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m52s
apple / screenshots (push) Successful in 9m36s
windows-host / package (push) Successful in 19m10s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Successful in 20s
|
||
|
|
78efedc0d8 |
release: 0.29.0 — version bump, notes, CHANGELOG, Play notes
apple / swift (pull_request) Successful in 2m3s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 2m52s
android / android (pull_request) Successful in 7m14s
ci / web (pull_request) Successful in 1m23s
ci / bun-nix (pull_request) Successful in 17s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 6m27s
ci / docs-site (pull_request) Successful in 1m27s
ci / rust-arm64 (pull_request) Successful in 15m57s
nix / flake (pull_request) Successful in 13m40s
ci / rust (pull_request) Successful in 31m1s
53 commits since v0.28.1 (36 non-merge). Cut from origin/main |
||
|
|
8c6099da2a |
Merge pull request 'The mgmt-port learn landed without its Windows half — restore the three missing pieces' (#241) from worktree-windows-client-mgmt-port-fix into main
ci / docs-site (push) Successful in 1m22s
ci / bun-nix (push) Successful in 24s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m4s
android / android (push) Successful in 8m32s
deb / build-publish-gamescope (push) Successful in 29s
deb / build-publish-client-arm64 (push) Successful in 3m24s
deb / build-publish (push) Successful in 8m24s
ci / rust-arm64 (push) Successful in 11m45s
deb / build-publish-host (push) Successful in 5m6s
ci / web (push) Successful in 11m58s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 6m40s
arch / build-publish (push) Successful in 17m15s
flatpak / build-publish (push) Successful in 8m43s
deb / smoke-install (push) Successful in 3m45s
ci / rust (push) Successful in 26m55s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m50s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 9s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 6s
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/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (push) Successful in 6s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 7s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / builders-arm64cross (push) Successful in 8s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 49s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m18s
docker / deploy-docs (push) Successful in 1m9s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 29m36s
Reviewed-on: #241 |
||
|
|
2b066b3e11 |
fix(clients): the mgmt-port learn landed without its Windows half — restore the three missing pieces
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 2m50s
ci / rust-arm64 (pull_request) Successful in 2m40s
ci / bun-nix (pull_request) Successful in 3m13s
ci / docs-site (pull_request) Successful in 4m3s
ci / web (pull_request) Successful in 4m5s
android / android (pull_request) Successful in 5m48s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 6m21s
ci / rust (pull_request) Successful in 11m34s
|
||
|
|
c3c24b5855 |
Merge pull request 'Hyprland streaming worked for exactly one session, and the cursor was never on the streamed head — six bugs, all ours' (#240) from worktree-capture-bgra-dmabuf-pod into main
ci / rust-arm64 (push) Failing after 2m40s
android / android (push) Successful in 8m39s
ci / bun-nix (push) Successful in 18s
arch / build-publish (push) Successful in 9m22s
windows-host / package (push) Successful in 12m24s
windows-host / winget-source (push) Skipped
deb / build-publish (push) Successful in 3m58s
ci / web (push) Successful in 5m46s
deb / build-publish-gamescope (push) Successful in 29s
ci / rust (push) Canceled after 8m19s
ci / docs-site (push) Canceled after 5m6s
deb / build-publish-host (push) Canceled after 3m11s
deb / build-publish-client-arm64 (push) Canceled after 26s
deb / smoke-install (push) Canceled after 0s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 17s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 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/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
windows-host / canary-manifest (push) Successful in 2m8s
Reviewed-on: #240 |
||
|
|
0d4f878f32 |
Merge pull request 'The store's library shelves carry real cover art on every platform' (#239) from worktree-shot-cover-art into main
android / android (push) Canceled after 0s
ci / rust (push) Canceled after 0s
ci / rust-arm64 (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bun-nix (push) Canceled after 0s
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/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (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 2m5s
apple / distribute (push) Successful in 11m8s
apple / screenshots (push) Successful in 9m30s
Reviewed-on: #239 |
||
|
|
d886cd0124 |
Merge pull request 'The Windows signing docs still told users to import a certificate we no longer publish' (#238) from worktree-docs-azure-signing-cert-import into main
ci / rust (push) Canceled after 0s
ci / rust-arm64 (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bun-nix (push) Canceled after 0s
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/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (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
windows-host / package (push) Canceled after 0s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Failing after 1m48s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (push) Failing after 1m56s
Reviewed-on: #238 |
||
|
|
2832b5d0f6 |
fix(host): the park schedule read a missing cursor overlay as a lost pointer, but an Embedded portal never sends one
ci / rust-arm64 (pull_request) Successful in 3m19s
ci / web (pull_request) Successful in 2m19s
ci / bun-nix (pull_request) Successful in 1m8s
ci / rust (pull_request) Successful in 9m12s
ci / docs-site (pull_request) Successful in 8m38s
android / android (pull_request) Successful in 20m26s
Field report, working Hyprland stream: the pointer jumps to the screen centre once a second for ~10 s at the start of every session, fighting every mouse movement, then settles. That is `park_pointer`'s schedule running its full cap. Parking exists for a good reason — a pointer-locked client sends only RELATIVE deltas, so nothing would ever move the seat pointer onto a freshly created virtual output — and past its two unconditional attempts it keeps going only while a host-composite session STILL has no live cursor overlay. "No overlay ⇒ the pointer has not reached the streamed output" is sound on Mutter, which suppresses `SPA_META_Cursor` while the pointer is off the recorded view. It is meaningless on the whole wlr family: xdph and xdpw advertise `AvailableCursorModes = 3` (Hidden|Embedded), so a session that asks for metadata is served EMBEDDED — the compositor paints the pointer into the frames and sends no cursor metadata, ever, wherever the pointer is. The heuristic was reading noise and warping the user's pointer over it. Distinct from — and complementary to — 5a5397ca, which fixed WHERE the warp landed (the wlr virtual pointer was bound to the operator's head, so the park drove a screen nobody was streaming). That one makes the park work; this one stops it repeating on evidence that does not exist. Both are needed: with only 5a5397ca the pointer would be re-centred on the *streamed* output once a second instead, which is the field report's symptom exactly. The same fact broke a second thing next to it. `metadata_composite` had the host plan a metadata cursor composite on a backend that can never deliver metadata: the stream logged "host-composite active but the capture has no live cursor overlay" for its whole life and drew no host pointer, which is why an earlier session on this box looked cursorless. Under Embedded the compositor's burnt-in pointer IS the cursor, and the host must not plan a composite at all. So surface what the portal actually negotiated instead of inferring it: `portal_cursor::negotiate` now returns our own `Mode` (re-exported as `pf_vdisplay::PortalCursorMode`), the hyprland/wlroots portal threads carry it back beside the fd and node id — alongside, not instead of, the `closed_tx` teardown handshake and inside the same `HANDSHAKE_BUDGET` bound — and the backends, plus the monitor mirror that delegates to them, report it per session as `VirtualDisplay::last_portal_cursor_mode`. `None` is the default and what every non-portal backend reports (KWin `zkde_screencast`, Mutter `RecordVirtual`, gamescope, Windows all get the mode they ask for), so nothing about the GNOME behaviour this was built for changes. The host settles both consequences from that one fact in `settle_portal_cursor`, at bring-up and again after every capture-loss rebuild (the retarget arm has to recompute `metadata_composite` from the compositor alone, because it runs before the rebuild to set `hw_cursor`). `plan.cursor_blend` is deliberately left alone: it is resolved before any display exists, and pre-judging it would mean re-asserting what the wlr portals advertise — the exact hardcode `portal_cursor` exists to have deleted. It costs a colour conversion, not correctness. Also cuts the park RETRY for a client that steers the seat pointer itself. The doc claimed a desktop-model client "overrides it with its first absolute move, so the jump is invisible in practice" — one park at bring-up is, a repeat is not: such a client sends absolute positions, the very same event the park synthesizes, only aimed where the user is actually pointing. It keeps the single bring-up park, so the session's first click cannot land on whatever monitor the seat pointer was left on, and loses the retry that fights the user. A cold EIS connection swallows the client's own moves too, and those keep coming. On the reported session this is 2 parks in the first second instead of 11 over ten, and no phantom composite. Verified: `cargo fmt --all --check`; `scripts/xcheck.sh linux clippy` and `windows clippy`; `cargo clippy -p punktfunk-host -p pf-vdisplay -p pf-inject --all-targets --locked -- -D warnings` and `cargo test` for the three, run for Linux in the ci/rust-ci.Dockerfile image (this crate does not build on macOS at all — opus, zerocopy and the Linux-only vdisplay entry points are cfg'd out there, so the container is the only way to compile it). pf-vdisplay 231 tests, pf-inject 130+7, and both new tests pass. punktfunk-host's suite has two pre-existing failures under that emulated container — `gamestream::stream::tests::sender_delivers_ batches` (EINTR on a socket recv) and one of the two `mgmt` local-summary tests, which share process-global session state — and the SAME two fail on this branch's parent without this commit; each passes in isolation. Not verified on glass: the .138 Hyprland box is read-only and in use. |
||
|
|
6863f8141a |
fix(input): the wlr injector aimed absolute motion at the operator's head, never the streamed one
`WlrootsInjector::open` created its virtual pointer with `globals.output` — whatever
`wl_output` the registry roundtrip had bound, which was `state.output.is_none()`, i.e. the
FIRST one advertised. Registry globals arrive in creation order, so "first" is the
compositor's oldest output: the operator's physical head, never the per-session headless one
the client is looking at. The wlr protocol maps `motion_absolute` onto the output the pointer
was CREATED with ("if the output argument is set, the compositor should map the input device
to the requested output"), so every absolute sample from every session drove a screen nobody
was streaming. On the EXTEND backends — Hyprland and wlroots/sway, where the streamed head
sits beside the operator's — that is the field report "no cursor was visible in the session",
and it is also why `park_pointer`'s opening warp put the seat cursor on the operator's
desktop once a second instead of on the stream.
Not a startup race, though it looks like one. The host journal on the Hyprland box shows
BOTH orderings across sessions of the same build — the injector opening 3.7 s before the
headless output in one, 24 ms after it in another — and the bug in both, because
"first advertised" is the oldest global either way. `hyprctl monitors` on that box:
`HDMI-A-1` (ID 0) at +0+0, `PF-87756-3` (ID 1) at +1920+0. ID 0 is always first.
Three parts.
1. pf-vdisplay carries the head's compositor name out on `VirtualOutput::output_name`, the
Linux counterpart of what `win_capture` already carries on Windows. Set by hyprland and
wlroots (the two EXTEND backends) and by the monitor mirror; `None` on KWin/Mutter (they
inject through libei, which selects by region) and gamescope (it owns its whole seat).
Threaded through the registry pool so a keep-alive reuse answers with the same name a
fresh create would — no poolable backend sets it today, and this is so that stops being a
silent trap the day one does.
2. The host publishes it at capture bring-up, `pf_inject::set_stream_output`, in the same
place and shape as the Windows arm's existing `set_stream_target`.
3. The wlr injector binds EVERY `wl_output` at v4 (for the `name` event), matches the
published name, and re-creates its virtual pointer bound to that output whenever the
target changes — releasing any held button on the old device first, because nothing else
would and a virtual pointer destroyed mid-press leaves the host with a stuck button.
Matching is by NAME, with NO fallback, and the absence of the fallback is the fix: the old
"first output" behaviour WAS the fallback. Size could not stand in for it either —
`MouseMoveAbs`'s extent is the client's letterboxed video rect in its own window, not the
streamed mode, so no size ladder can identify the head. An unresolved target binds NO output,
which maps absolute coordinates over the whole layout: on a single-output compositor that is
identical to binding that output, and on a multi-head one it at least keeps the streamed head
reachable, unlike a pin to the wrong one.
`inject` now also READS the Wayland socket. It only ever called `dispatch_pending`, which is
documented to "not perform reads on the Wayland socket", so the queue held nothing but what
`open`'s roundtrips put there. Without this the retarget above would have been dead in
exactly the sessions that need it most — the injector could never learn about a `wl_output`
created after it opened — and, separately, everything the compositor sent had been piling up
unread in the socket buffer for the host's lifetime, including the protocol errors the
comment there claimed to be surfacing.
Concurrency, stated plainly: ONE slot per process. The injector is host-lifetime (in fact
there are two `InjectorService`s — the native plane's and one per GameStream control
listener) and `InputEvent` is an 18-byte `#[repr(C)]` ABI struct with no session field, so
with parallel sessions (up to `max_concurrent`, default 4) the LAST capture bring-up wins for
everyone's absolute input. That is the same trade `stream_target` already documents on
Windows, and it is strictly better than what it replaces, where every session aimed at a head
NO session was streaming. Making injection genuinely session-aware is the real fix and a much
larger one — it needs source-tagged input events through both control planes.
`set_absolute_anchor`'s warning is amended rather than quietly violated: it still must not be
called from a session path, and it now says which mechanism took the per-session trade, why
that is a separate slot (this one is the operator's host-wide capture pin, recomputed from
policy whenever the console writes it — which would wipe a per-session value), and where the
trade is written down.
Gates: `cargo clippy --all-targets -p pf-inject -- -D warnings`, `cargo build -p pf-inject`
and `cargo test -p pf-inject` (137 tests, incl. 3 new) on x86_64-unknown-linux-gnu in
`punktfunk-rust-ci`; `cargo check -p punktfunk-host` likewise; `scripts/xcheck.sh linux
clippy` plus 230 `pf-vdisplay` tests; `cargo fmt --all --check`. The two new `wlr` tests pin
the regression directly — an unknown target must bind NOTHING rather than fall back to the
first advertised output.
Not verified here: no on-glass run. The box at .138 is in live use and read-only to me, so
the change is unproven against a real Hyprland seat. Two smaller things also rest on reading
rather than observation — that Hyprland's `hyprctl` monitor name is byte-identical to its
`wl_output.name` (the protocol says the name is "the same for all clients", and xdph already
resolves our `hyprctl`-minted name to the same output for screencast, which is field-proven),
and the exact on-screen arithmetic of the "moves to mid-screen then jumps back" symptom,
which does not follow from the protocol's normalize-by-extent mapping and would need
Hyprland's own source to pin down.
|
||
|
|
cf4c12ea52 |
fix(vdisplay): a per-cast tokio runtime orphaned ashpd's process-global connection after one cast
THE reason the first stream of a host process worked and every later one was
black. Not xdph, not the compositor, not the formats — ours, and a lifetime
mistake.
ashpd caches its D-Bus connection process-globally:
static SESSION: OnceLock<zbus::Connection> // ashpd 0.13.13, src/proxy.rs:27
The first `Screencast::new()` in the process creates that connection, and zbus
spawns its background reader as a task on whichever tokio runtime is current at
that moment. Both wlr backends built their OWN multi-thread runtime per cast and
dropped it at teardown — so the first cast created the cached connection on a
runtime that was then destroyed with it, and the OnceLock went on handing the
same executor-less connection to every later `Screencast::new()`, which awaited
a reply nothing was alive to read.
Measured 2026-08-14 (Hyprland 0.55.4, xdph 1.3.12): first cast of a host process
streamed, every cast after it hung, and the surviving cast thread sat in
futex_do_wait inside runtime shutdown. The discriminator that pins it on us: a
freshly spawned process completed the identical handshake against the identical
xdph, repeatedly, while the long-lived host completed none — with xdph itself
idle at 28 ms of CPU, so it was never the one wedged. Teardown was already
correct by then: the log shows `hyprland headless output removed` in the right
order.
One shared runtime (`portal_rt`), built once, never dropped, `block_on(&self)`
from every cast thread. It outlives the cached connection because it must.
Also bound `Screencast::new()` itself, not just the handshake after it: with the
connection orphaned that call is exactly where the thread hung, so the earlier
bound started one step too late and the failure still surfaced as the caller's
generic 20 s timeout.
Verified: cargo fmt --all --check, scripts/xcheck.sh linux clippy (-D warnings),
cargo test -p pf-vdisplay 122 passed. Not verified: on-glass.
|
||
|
|
5e5d6904d3 |
fix(vdisplay): a hung portal handshake leaked its thread, and one leak poisoned every later cast
MEASURED 2026-08-14 on the Hyprland box, with the ordered-teardown fix already
in place. The first cast of a host process streamed:
hyprland headless output ready … output=PF-44694-1 w=5120 h=1440 hz=240
pipewire stream state old=Paused new=Streaming
and every cast after it timed out in select_and_cast. The host had NINE live
`punktfunk-hypr-cast` threads and 28 tokio workers at that point.
`select_sources`/`start` await a D-Bus reply that a wedged portal never sends.
That await cannot be cancelled by the `stop` flag, because the flag is only
read by the park loop further down — a thread stuck in the handshake never
reaches it. So every timed-out attempt left a thread parked forever on a
half-created portal session, holding this process's shared D-Bus connection,
and from the first hang onwards every later request from the SAME process hung
too.
The discriminator that proves it is the process, not the portal: a freshly
spawned process (`punktfunk-host spike --source portal`, driven through the
same custom picker) completed the identical handshake against the very same
xdph — repeatedly — while the long-lived host could not complete any. xdph
itself was idle, 28 ms of CPU since start, so it was not spinning.
Bound the handshake at 15 s, under select_and_cast's 20 s wait so the failure
is reported by the thread that owns it, with a reason, and — the point — so
that thread EXITS instead of leaking. Same change in the wlroots/sway backend:
xdpw carries the identical unbounded node-id spin (screencast.c), so it can
wedge the same way.
Verified: cargo fmt --all --check, scripts/xcheck.sh linux clippy (-D warnings),
cargo test -p pf-vdisplay 122 passed. Not verified: on-glass — needs the box.
|
||
|
|
9ce347e4c0 |
fix(vdisplay): we removed the captured output before closing the cast, and xdph spun on the wreckage
Only the FIRST stream after a portal start ever worked on Hyprland; every
one after it died in `select_and_cast` with
create virtual output: timed out waiting for the ScreenCast portal on PF-…
The mitigation on `worktree-capture-bgra-dmabuf-pod` chased the symptom. This
is the mechanism, read out of xdph 1.3.12's source and the box's own journal.
WE YANK THE OUTPUT OUT FROM UNDER A LIVE CAST. `Keepalive` drops `StopGuard`
then `OutputGuard`, and `StopGuard::drop` only SET an atomic and returned. The
portal thread noticed 200 ms later and merely dropped its zbus connection. So
`hyprctl output remove` ran — synchronously, microseconds later — on an output
xdph was still capturing, every single teardown.
Nothing closed the session either. xdph destroys one on exactly one event, an
explicit `org.freedesktop.impl.portal.Session.Close` (`Session.cpp:37`); it has
no peer-vanished watcher. The frontend does (`xdg-desktop-portal.c:230`
`peer_died_cb` → `close_sessions_for_sender`), but only once our bus name goes
away — after the poll, asynchronously, on a GTask thread. Long after the output
is gone. Proof from the box: xdph's toplevel lock stayed at 2 for 4.5 minutes
after our stream ended and its output was removed, and that session's
`Session destroyed` never came.
XDPH THEN SPINS AT 100% CPU, FOREVER. Handed that wreckage, `startSharing`
falls into `Screencopy.cpp:307-313`
while (pSession->sharingData.nodeID == SPA_ID_INVALID) {
int ret = pw_loop_iterate(g_pPortalManager->m_sPipewire.loop, 0);
— timeout 0, i.e. NON-blocking, i.e. an unbounded hot spin on xdph's only
event-loop thread, inside the `Start` handler, holding its `m_mEventLock`. From
there it answers no D-Bus, no Wayland, no PipeWire, ever again. MEASURED: the
wedged instance's unit reported `Consumed 3min 51.971s CPU time over 23min
41.092s wall clock`, and there were 232.70 s of wall clock between its last log
flush and its restart — 231.971 s of CPU against 232.70 s of wall, one core
pinned solid for precisely the wedged interval.
Everything after that is queueing. Our next handshake gets nothing, times out at
20 s, and `SelectionFile` deletes the per-session selection file on its way out;
if xdph is restarted mid-queue it finally runs the picker for that stale request
and reads an empty file — the `SHAREDATA returned selection -1` in the log.
THE FIX IS THE ORDER. `StopGuard::drop` now signals and then WAITS for the
portal thread to have closed the ScreenCast session, and only then does
`OutputGuard` remove the output. The close is answered synchronously by the
frontend (`xdp-session.c:217` `handle_close` → `xdp_dbus_impl_session_call_close_sync`),
so when it returns xdph has already run `destroyStream`. The output we remove
next is one nobody is capturing. Bounded at 3 s on each side — an already-wedged
portal must not be able to wedge our teardown with it — and the park poll drops
to 20 ms now that teardown waits on it.
wlroots gets the same change, and NOT on an assumption of symmetry: xdpw was
read to confirm both preconditions. `src/core/session.c` gives its session
object exactly one method, `Close`; and `src/screencast/screencast.c:599-605` is
the identical unbounded `while (cast->node_id == SPA_ID_INVALID)` spin — xdph's
copy is that code. sway's `output unplug` yanks a captured output exactly the
way Hyprland's `output remove` did. Not observed on glass; no sway box.
THE PICKER LINE WAS ALSO MALFORMED, AND IS A RED HERRING FOR THE STALL. xdph
splits the picker's line on the first `/` into flags and selection
(`ScreencopyShared.cpp:86-87`) and we never sent one. `find_first_of` then
returns npos, so FLAGS became the whole payload — and SEL became the whole
payload too, purely because `npos + 1` wraps to 0, which is why the output name
still parsed and why this hid. What did not hide is the flag loop walking
`screen:<name>` one character at a time (`unknown flag from share-picker: s`,
`c`, `e`, …) and setting `allowToken` on the `r` of `sc*r*een`, so xdph answered
every Start with a `restore_data` + `persist_mode: 2` we never asked for. The
reference picker prints the separator unconditionally
(`hyprland-share-picker/main.cpp:133-136`), so empty flags are a bare leading
`/`. Fixed to `[SELECTION]/screen:<NAME>`.
It is NOT what stalled anything: the sessions that streamed fine logged the
identical flag spam and the identical restore token, so it never discriminated.
The format moves to `portal_picker.rs`, declared unconditionally like
`portal_config` and `portal_cursor`, with xdph's parser transcribed into the
tests — including the npos arithmetic. A wire format with no schema and no error
report is invisible from the string alone: the old line's one assertion passed
the entire time it was wrong. Those tests now run on every platform's CI rather
than only the leg that compiles `mod hyprland`.
Verified: cargo fmt --all --check, scripts/xcheck.sh linux clippy AND windows
clippy (-D warnings), cargo test -p pf-vdisplay 122 passed (5 new). Not verified:
on-glass behaviour — the box is in use for live testing and read-only to me.
Upstream bugs worth filing, both wlr-family: the unbounded node-id spin
(hyprwm/xdg-desktop-portal-hyprland Screencopy.cpp:307,
emersion/xdg-desktop-portal-wlr screencast.c:599) should be bounded and fail the
request rather than pinning a core forever; and xdph's picker parse should reject
a line with no `/` instead of reading the whole selection as flags.
|
||
|
|
dea6395772 |
fix(capture): xdph offers BGRA on its dmabuf pod, and we offered BGRx, so the link never negotiated
A Hyprland/sway client went black with no error of ours: PipeWire failed the
link itself with
pw.link: (73.0.0 -> 81.0.0) negotiating -> error no more input formats (-22)
Measured on Hyprland 0.55.4 + xdph 1.3.12 by dumping both EnumFormat pods from
the PipeWire DAEMON (`PIPEWIRE_DEBUG=*:1,pw.link:5` — the pods are not in our
own process's log, which is why this hid for so long):
ours BGRx only | 12 NVIDIA tiled + 0 (LINEAR) | MANDATORY
xdph BGRA only | the same 12 + MOD_INVALID | MANDATORY|DONT_FIXATE
xdph BGRA or BGRx, no modifier | (the SHM pod)
The modifiers intersect perfectly. Only the fourcc never does, which is exactly
why the failure reads as a GPU/modifier problem and is not one — the host's own
message ("the compositor never accepted the dmabuf-only offer (EGL->CUDA GPU
import)") points at the GPU, and our advert line prints only the first 6 of 13
modifiers so LINEAR is invisible. Both misled a full session of debugging.
Since our offer is dmabuf-only, xdph's mixed SHM pod could not rescue it.
Offer a BGRA dmabuf pod beside the BGRx one. BGRA and BGRx are the same 32-bit
layout, the alpha byte is ignored all the way to the encoder (`vk_util` maps
both to B8G8R8A8_UNORM, VAAPI both to Pixel::BGRA), and the import is driven by
the NEGOTIATED format's fourcc, so an AR24 frame imports as AR24.
Vendor-neutral by construction: the two modifier lists are enumerated PER FOURCC
(`XR24` and `AR24` asked separately), because EGL and libva answer per format and
nothing entitles us to assume a driver importing one imports the other. On the
VAAPI passthrough path there is no importer, so both lists are LINEAR (plus the
PyroWave Vulkan set when armed) — AMD and Intel get the BGRA pod on the same
terms as NVIDIA rather than an NVIDIA-shaped guess.
The BGRA pod is listed AFTER BGRx, so a producer offering both still lands on
the pre-existing path — first compatible consumer pod wins, so this is purely
additive. Both pods are now guarded on a non-empty list (`build_dmabuf_format`
indexes `modifiers[0]`).
Also name `linear_offered` and both counts in the advert log, so the truncated
`sample` can no longer be misread as the whole offer.
|
||
|
|
55dbb14cf4 |
docs(windows): the signing pages still told users to import a certificate we no longer publish
ci / bun-nix (pull_request) Successful in 20s
ci / web (pull_request) Successful in 1m2s
ci / rust-arm64 (pull_request) Successful in 1m22s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Failing after 1m47s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Failing after 1m53s
ci / docs-site (pull_request) Successful in 8m3s
ci / rust (pull_request) Successful in 27m30s
Azure Artifact Signing chains to a public root, so neither the host installer nor the client MSIX ships a .cer any more and users import nothing. install.md, install-client.md and windows-host.md still walked through importing one — and because the pre-Azure files are still sitting in the package registry those URLs return 200 rather than 404, so following the docs didn't fail loudly, it quietly planted a retired self-signed cert in machine Root and TrustedPublisher. Verified on glass against the 0.29 canary while checking the release: both artifacts verify Valid, timestamped, and publicly trusted, and the MSIX installs with nothing imported. Also corrected while here: the MSIX publisher change makes a different package identity, so installs from 0.28.1 or earlier need an uninstall rather than an upgrade (and a packaged app's settings go with it, so the client pairs again); a silent UPGRADE reuses the task selection the previous install recorded instead of the wizard defaults, so a once-declined installgamepad silently keeps stale gamepad drivers; installaudiocable stopped being a task name in 4a621de6; and Add-AppxPackage from a non-interactive session can fail 0x80070005 when the Windows App Runtime it depends on is in use. |
||
|
|
f0b35de92a |
feat(screenshots): the library shelf carries real cover art on every platform
apple / swift (pull_request) Successful in 2m5s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 3m15s
ci / web (pull_request) Successful in 1m34s
ci / docs-site (pull_request) Successful in 1m27s
ci / bun-nix (pull_request) Successful in 39s
android / android (pull_request) Successful in 7m21s
ci / rust (pull_request) Successful in 7m50s
The Apple scenes passed artLoader nil, so the store's library frames shipped empty frosted cards next to Android's populated shelf. LibraryArtSource (a two-method protocol the production LibraryArtLoader already satisfies) lets the harness answer art from a canned source, and the four posters are drawn procedurally at capture time — CoreGraphics on Apple, Canvas on Android, same designs, same seeds — replacing Android's gradient-plus-monogram tiles. Nothing is bundled; release builds ship none of it. |
||
|
|
588962f696 |
Merge pull request 'A lost merge run left an Android fix unpublished, and no re-run could ship it' (#237) from worktree-android-dispatch-publish-hatch into main
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 8s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 9s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 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
docker / builders (ci/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (push) Successful in 12s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 13s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m13s
docker / builders-arm64cross (push) Successful in 7s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m27s
docker / deploy-docs (push) Successful in 32s
android / android (push) Successful in 7m4s
ci / rust (push) Canceled after 0s
ci / rust-arm64 (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bun-nix (push) Canceled after 0s
Reviewed-on: #237 |
||
|
|
91b8f1a939 |
ci(android): a manual dispatch can publish, so a lost merge run is not a dead end
ci / web (pull_request) Successful in 1m1s
ci / docs-site (pull_request) Successful in 1m18s
ci / bun-nix (pull_request) Successful in 1m37s
android / android (pull_request) Successful in 4m33s
ci / rust-arm64 (pull_request) Successful in 12m3s
ci / rust (pull_request) Canceled after 29m33s
Merging two PRs seconds apart can leave the older merge sha with no run at all - Gitea attributes the window's runs to the newer head. |
||
|
|
b5cace3a00 |
Merge pull request 'The app menu ate ⌘Q, so the host's compositor never saw the chord' (#236) from worktree-apple-cmd-passthrough into main
apple / swift (push) Successful in 2m3s
apple / distribute (push) Successful in 10m51s
apple / screenshots (push) Successful in 8m49s
ci / rust (push) Successful in 27m48s
ci / web (push) Successful in 9m48s
ci / docs-site (push) Successful in 8m58s
ci / bun-nix (push) Successful in 27s
ci / rust-arm64 (push) Successful in 2m3s
Reviewed-on: #236 |
||
|
|
1e5dca4c25 |
Merge pull request 'A pad whose Select is KEYCODE_BACK quit the session on one press' (#235) from worktree-shield-select-back-quit into main
Reviewed-on: #235 |
||
|
|
b2146f33fe |
fix(apple): the app menu ate ⌘Q, so the host's compositor never saw the chord
ci / rust (pull_request) Successful in 6m40s
ci / web (pull_request) Successful in 1m11s
ci / docs-site (pull_request) Successful in 2m13s
ci / bun-nix (pull_request) Successful in 46s
apple / swift (pull_request) Successful in 2m5s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 1m58s
With the default modifier layout ⌘ is Super on the host, which makes ⌘Q the chord
a Hyprland/KDE/GNOME user reaches for first. AppKit dispatches menu key
equivalents before the stream view ever sees a keyDown, so it quit the client
instead. Not Hyprland-specific.
`InputCapture`'s local keyDown monitor now claims every ⌘ chord while input is
captured and forwards it to the host itself. It has to send from there: the
monitor runs ahead of BOTH the menu and `StreamLayerView.keyDown`, and on macOS
that second one is the host's only key path (the GCKeyboard send has been
iOS-only since
|
||
|
|
1ac6c9bf3d |
fix(android): a pad whose Select is KEYCODE_BACK quit the session on one press
ci / bun-nix (pull_request) Successful in 21s
ci / docs-site (pull_request) Successful in 1m23s
ci / rust-arm64 (pull_request) Successful in 2m50s
ci / web (pull_request) Successful in 5m55s
ci / rust (pull_request) Canceled after 6m47s
android / android (pull_request) Successful in 5m38s
Field report: pressing Select disconnected the stream. The host log was
unambiguous about what it was NOT — "client datagram stream ended" plus "virtual
display torn down (deliberate quit — keep-alive skipped)" is a client that said
it was leaving, not a drop and not a compositor crash.
Only two client paths raise that: StreamScreen's BackHandler, and the exit chord
(router.onExitChord). The chord is excluded by construction — `armExit` posts a
1 s timer and releasing any member calls `disarmExit`, so a tap always cancels.
That leaves the back stack, and from a SOURCE_GAMEPAD device KEYCODE_BACK is the
ONLY keycode that reaches it: a mapped button is consumed in the gamepad branch,
anything with a VK is consumed on the keycode path, volume/power go to the
system, and a FLAG_FALLBACK BACK is swallowed. So a one-press quit identifies the
button's keycode without knowing which controller was on the couch.
Plenty of pads deliver Select as the plain KEYCODE_BACK a remote's Back uses,
with no BUTTON_SELECT scancode behind it — the Android-TV shape, where every
input device is expected to offer Back, reached whether the vendor prints "Back"
on the button or "Select"/"View". `buttonBit` had no row for KEYCODE_BACK, so the
press fell through unconsumed into StreamScreen's BackHandler, which is the
deliberate-quit exit. One press, session over.
The same gap meant those pads could not produce BTN_BACK at all, so every
shortcut built on Select was unreachable on exactly the devices whose users have
no keyboard: the emergency exit chord StreamScreen's own start banner advertises
("Hold Select + Start + L1 + R1 to leave"), the mic mute, the stats tier.
New `Gamepad.padButtonBit(keyCode, flags)` — buttonBit plus that one row —
resolves a gamepad-sourced BACK to BTN_BACK, and MainActivity's streaming branch
asks it instead. It keys off the keycode, not the vendor, so it covers every pad
with this behaviour; a pad that does carry BUTTON_SELECT is unaffected in both
directions, having never had the bug. FLAG_FALLBACK events stay excluded: those
are the synthetic BACK the framework raises after an unconsumed BUTTON_* press,
and forwarding one would put a phantom Select on the wire (one landing while
Start + L1 + R1 were held would complete the exit chord out of nowhere). A
remote's or keyboard's BACK is neither mouse- nor gamepad-sourced, so it still
leaves the stream — for a device with no pad on it that is the documented way
out, and the banner says so.
The mouse-side-button hook moves above the gamepad branch so a device that can
be a mouse keeps its X1/X2 semantics; it answers null for everything that cannot
be a mouse, so nothing else changes route.
PadButtonBitTest pins the mapping, the fallback exclusion, that the three Select
chords are now reachable from a BACK-only pad, and that no other keycode moved.
Verified: :kit:testDebugUnitTest + :app:testDebugUnitTest green (PadButtonBitTest
4/4), :app:compileDebugKotlin clean. NOT yet verified on-glass — the behaviour
needs a real pad: Select reaches the game, and Back no longer quits.
|
||
|
|
36e133ae66 |
Merge pull request 'Android canary ships to Play open + closed testing instead of internal' (#234) from worktree-android-canary-open-testing into main
ci / web (push) Successful in 1m30s
ci / bun-nix (push) Successful in 17s
ci / rust-arm64 (push) Failing after 2m32s
ci / docs-site (push) Successful in 1m45s
android / android (push) Successful in 7m59s
ci / rust (push) Canceled after 17m30s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 11s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 12s
docker / builders (ci/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (push) Successful in 12s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m20s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 52s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 27s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / builders-arm64cross (push) Successful in 9s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 48s
docker / deploy-docs (push) Successful in 36s
Reviewed-on: #234 |
||
|
|
d7e66fafe1 |
Merge pull request 'The store's four marketing frames become screenshot scenes on both platforms' (#233) from worktree-store-marketing-scenes into main
android / android (push) Canceled after 17s
ci / rust (push) Canceled after 0s
ci / rust-arm64 (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bun-nix (push) Canceled after 0s
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/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (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 1m59s
apple / distribute (push) Successful in 11m3s
apple / screenshots (push) Canceled after 7m31s
Reviewed-on: #233 |
||
|
|
a4210024dc |
ci(android): canary also feeds Play closed testing (alpha)
android / android (pull_request) Successful in 7m7s
ci / rust (pull_request) Successful in 7m38s
ci / web (pull_request) Successful in 2m25s
ci / docs-site (pull_request) Successful in 4m18s
ci / bun-nix (pull_request) Successful in 2m40s
ci / rust-arm64 (pull_request) Successful in 2m23s
Closed testing went dry on 2026-08-01 when tags started publishing straight to production instead of alpha — its testers have been pinned to the last pre-access build since. Canaries now assign the same versionCode to beta (open) AND alpha (closed) via play-upload.py's new repeatable --also-track flag: both PUTs share one Play edit, so one commit and one review cover both tracks and they can never disagree about which canary is current. Tags still go to production only. |
||
|
|
ed935ed31c |
ci(android): canary ships to Play open testing (beta), not internal
Main-push canaries now land on the open-testing track: public opt-in link, no tester-list cap. Trade-off documented in the workflow header: open testing goes through Google review (hours/days), where internal was review-free (minutes). android-promote's from_track default follows the canary to beta. |
||
|
|
daabb85373 |
Merge pull request 'The Linux data-plane renice was a silent no-op on every install — RealtimeKit fallback, audio threads boosted at all, nice-limit headroom on every channel' (#232) from worktree-thread-qos-rtkit into main
audit / bun-audit (sdk) (push) Successful in 36s
audit / bun-audit (web) (push) Successful in 14s
audit / docs-site-audit (push) Successful in 26s
audit / pnpm-audit (push) Successful in 18s
audit / cargo-audit (push) Successful in 2m26s
apple / swift (push) Successful in 1m56s
audit / bun-audit (plugin-kit) (push) Successful in 3m33s
ci / rust-arm64 (push) Failing after 2m12s
audit / miri (push) Successful in 5m14s
android / android (push) Successful in 8m30s
ci / bun-nix (push) Successful in 26s
ci / docs-site (push) Successful in 1m35s
audit / license-gate (push) Successful in 8m7s
audit / c-abi-asan (push) Successful in 8m29s
ci / web (push) Successful in 6m5s
apple / distribute (push) Successful in 10m58s
apple / screenshots (push) Successful in 8m44s
windows-host / package (push) Successful in 13m15s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 31s
ci / rust (push) Canceled after 29m44s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Failing after 1m49s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (push) Failing after 1m49s
flatpak / build-publish (push) Successful in 33m23s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 8s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / builders (ci/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (push) Successful in 8s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 27s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 29s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 31s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 31s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 25s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 22s
docker / builders-arm64cross (push) Successful in 11s
docker / deploy-docs (push) Successful in 38s
deb / build-publish-gamescope (push) Successful in 1m3s
deb / build-publish (push) Successful in 4m9s
deb / build-publish-client-arm64 (push) Successful in 6m0s
arch / build-publish (push) Successful in 8m3s
deb / build-publish-host (push) Successful in 8m35s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 8m57s
deb / smoke-install (push) Canceled after 1m17s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 18m5s
nix / flake (push) Failing after 26m33s
|
||
|
|
9af894a374 |
docs: changelog for the Linux thread-priority fix
ci / bun-nix (pull_request) Successful in 32s
nix / flake (pull_request) Failing after 44s
ci / rust-arm64 (pull_request) Failing after 51s
android / android (pull_request) Failing after 53s
ci / web (pull_request) Successful in 1m15s
apple / swift (pull_request) Successful in 2m2s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 3m19s
ci / docs-site (pull_request) Successful in 4m53s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 6m32s
ci / rust (pull_request) Successful in 17m59s
|
||
|
|
52df9c59af |
pkg(linux): nice-limit headroom on every channel, so the renice also works without rtkit
A new shared drop-in, packaging/linux/50-punktfunk-nice.conf (user@.service.d, LimitNICE=-15), raises the user-session nice hard limit so the direct setpriority() path works on rtkit-less boxes — a limit, not a grant, effective from the next login. Shipped by rpm (%files + install, flows into the Bazzite sysext via rpm2cpio), Arch, and deb; the Steam Deck installer writes it to /etc/systemd/system/user@.service.d instead (SteamOS /usr is read-only), following its existing sudo-to-/etc pattern. rpm and deb gain a weak Recommends: rtkit and Arch an optdepends hint — with rtkit the fix needs no relogin at all. The NixOS module instead sets security.rtkit.enable = mkDefault true (rtkit is not a given there; mkDefault keeps it operator-overridable). It remains true on every channel that the host binary must never carry a file capability — the spec's no-caps note now names the two fallback rungs instead of calling the thread nice a best-effort no-op. |
||
|
|
b21b2f6ce9 |
fix(host): the Linux data-plane renice was a silent no-op everywhere — fall back to RealtimeKit, and boost the audio threads at all
Every Linux host to date ran its capture/encode/send threads at nice 0: boost_thread_priority's setpriority() needs CAP_SYS_NICE or a raised RLIMIT_NICE, no install channel granted either, and the host binary can never carry a file capability (a capped process's /proc/<pid>/exe is unreadable to KWin — the 0.26.0-1 incident). A 2026-08-14 field log showed the cost end to end: a fresh game launch's shader-compile storm descheduled the unprioritized threads, 5 ms audio datagrams left late enough to stutter, the client's OWD signal rose, and ABR cut a gigabit-Ethernet session to its 5 Mbps floor at zero loss — while the same box carried 708 Mbps cleanly once the storm passed. The renice now falls back to RealtimeKit (MakeThreadHighPriorityWithPID, one blocking system-bus call per boosted thread) — the same unprivileged broker PipeWire clients use, so nothing enters the permitted set and KWin identification is untouched. Only the nice verb, never MakeThreadRealtime: the SCHED_RR reservations apply to rtkit-granted RR too. zbus rides ashpd's exact backend choice (tokio, no async-io) plus blocking-api, so the resolved graph gains no second I/O backend. And the audio plane is boosted for the first time: the 5 ms Opus capture->encode->send loop (critical — a stall there is directly audible), the PipeWire capture mainloop thread (its process callbacks run there; PipeWire's own module-rt only covers data loops we don't use), and the pad-audio streamer (above-normal, like the session send thread). The first two had no boost call at all; on Windows the audio_thread boost also engages, via the SetThreadPriority arm. |
||
|
|
791dedd62a |
Merge pull request 'The management port is movable, and the client no longer needs mDNS to find it' (#230) from worktree-mgmt-port-single-source into main
arch / build-publish (push) Failing after 58s
ci / rust-arm64 (push) Successful in 1m37s
ci / bun-nix (push) Successful in 19s
apple / swift (push) Successful in 1m57s
deb / build-publish-gamescope (push) Failing after 46s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Failing after 1m37s
deb / build-publish (push) Successful in 5m9s
deb / build-publish-host (push) Successful in 4m37s
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 7s
android / android (push) Successful in 7m24s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (push) Failing after 1m45s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 8s
docker / builders (ci/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (push) Successful in 8s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
deb / build-publish-client-arm64 (push) Failing after 2m43s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
ci / web (push) Successful in 7m11s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 46s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m20s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 1m12s
docker / builders-arm64cross (push) Failing after 6s
docker / deploy-docs (push) Failing after 2s
ci / docs-site (push) Successful in 8m48s
nix / flake (push) Failing after 3m33s
deb / smoke-install (push) Successful in 3m17s
apple / distribute (push) Successful in 11m8s
ci / rust (push) Successful in 19m53s
apple / screenshots (push) Successful in 8m49s
flatpak / build-publish (push) Successful in 16m36s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m15s
windows-host / package (push) Successful in 18m5s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Failing after 1s
Reviewed-on: #230 |
||
|
|
35f940a3bb |
fix(presenter): name the Connected callback type — widening it tripped clippy::type_complexity
apple / swift (pull_request) Successful in 1m59s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 1m15s
ci / rust-arm64 (pull_request) Failing after 1m49s
ci / docs-site (pull_request) Successful in 1m19s
android / android (pull_request) Successful in 5m27s
ci / rust (pull_request) Successful in 5m43s
ci / bun-nix (pull_request) Successful in 6m14s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Failing after 1m40s
nix / flake (pull_request) Successful in 14m6s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Failing after 2m0s
Adding the mgmt port beside the fingerprint pushed the inline `Option<Box<dyn FnMut([u8; 32], u16)>>` over `clippy::type_complexity`, which CI denies. A named `ConnectedFn` is what the lint asks for, and it gives the two positional arguments somewhere to be documented. My local gates ran `cargo check`, not `clippy -D warnings`, which is exactly why this reached CI instead of dying locally. Re-verified with `cargo clippy --all-targets -- -D warnings` across all eight crates: exit 0, pf-presenter confirmed genuinely linted, no type_complexity remaining. |
||
|
|
aa53f1e5ef |
Merge pull request #231 from fix-host-cer-alias-null-key
ci / web (push) Successful in 1m4s
ci / bun-nix (push) Successful in 19s
ci / rust-arm64 (push) Successful in 1m34s
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/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / builders (ci/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (push) Successful in 7s
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 6s
ci / docs-site (push) Successful in 1m12s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Failing after 49s
docker / builders-arm64cross (push) Skipped
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 17s
docker / deploy-docs (push) Successful in 42s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m58s
ci / rust (push) Canceled after 16m39s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (push) Canceled after 4m4s
windows-host / package (push) Canceled after 0s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
An unset HOST_CER_PATH is $null, and a null hash key is fatal — not an empty key |
||
|
|
80061fbf6b |
fix(ci): an unset HOST_CER_PATH is $null, and a null hash key is fatal — not an empty key
ci / rust-arm64 (pull_request) Failing after 1m42s
ci / bun-nix (pull_request) Successful in 16s
ci / web (pull_request) Successful in 1m4s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 2m43s
ci / docs-site (pull_request) Successful in 8m29s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 6m18s
ci / rust (pull_request) Failing after 21m4s
Azure signing produces no .cer, so HOST_CER_PATH is deliberately unset. The publish step then built its alias map as a single hash literal containing $env:HOST_CER_PATH as a KEY, and an unset $env: var is $null — "A null key is not allowed in a hash literal", which failed the whole step. Canary run 18256: the installer signed fine and published to its versioned path, then this line killed the alias refresh, so `canary/punktfunk-host-setup.exe` went stale. I reasoned about this line while making the .cer optional and concluded an unset variable would give an empty-string key, which is legal. It does not — that only happens through string interpolation. The $files guard just above filters the missing .cer correctly; the hash literal ran before anything could use it. Build the map incrementally instead, adding the .cer entry only when there is one, so the legacy .pfx modes still alias it. windows-client.yml survived the same change only by accident: it writes "$($env:MSIX_CER_PATH)", and interpolating $null yields an empty string, which IS a legal key. Made that explicit too rather than leaving correctness resting on quotes someone could reasonably tidy away. Verified under pwsh 7: the old literal reproduces the exact CI message with the var unset; the new form yields one entry unset and two entries set, with the .cer alias intact. |
||
|
|
026dbe6153 |
fix(screenshots): captures grow the status bar Robolectric never had
ci / rust-arm64 (pull_request) Successful in 1m57s
ci / bun-nix (pull_request) Successful in 48s
ci / docs-site (pull_request) Successful in 1m55s
apple / swift (pull_request) Successful in 2m3s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 8m58s
android / android (pull_request) Successful in 6m12s
ci / rust (pull_request) Successful in 27m10s
Robolectric renders no system UI and zero insets, so every phone capture was missing the status bar and its content sat where the bar belongs — on the Pixel store render the app title collided with the camera punch-hole. ShotStatusFrame draws a plausible bar (time left, radios right, the CENTRE left empty for the hole) and pushes the scene below it, the same geometry real insets produce; height mirrors a Pixel's tall bar measured off a real capture. On for the touch screens, off for the immersive surfaces (stream, console shell, TV) that hide the real bar too. |
||
|
|
3cc8fa7ee0 |
feat(core): the host tells the client where its library is, so mDNS is no longer required
ci / rust (pull_request) Failing after 10m40s
ci / rust-arm64 (pull_request) Failing after 31s
android / android (pull_request) Failing after 1m9s
ci / docs-site (pull_request) Successful in 1m23s
apple / swift (pull_request) Successful in 2m4s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Failing after 1m47s
ci / bun-nix (pull_request) Successful in 2m23s
ci / web (pull_request) Successful in 5m45s
nix / flake (pull_request) Successful in 13m46s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Failing after 1m47s
ABI 19 -> 20. Wire protocol unchanged (still 2). Persisting the mgmt port (fe2bfeca) made a moved port survive mDNS going away, but mDNS was still the only SOURCE: a host that had never been seen on it — VPN-only, a routed subnet, or simply added by address on a network where multicast has never worked — had nothing to learn from and fell back to 47990. The `Welcome` now carries the port, so the client learns it over the connection it has already authenticated and discovery stops being involved at all. `Welcome.mgmt_port`, a trailing u16 after the cipher block, following the same additive discipline as the eight fields before it (compositor, gamepad, bitrate_kbps, bit_depth, color, chroma_format, audio_channels, codec): an older peer stops earlier and gets a documented default, in both directions, so WIRE_VERSION does not move. ⚠ THE TRAP, and why emitting the port forces the `cipher` placeholder: `cipher` is emitted only when non-default, so appending the port to an AES Welcome would land its LOW BYTE at offset 68 — exactly where every shipped 0.28.x client reads `cipher`, whose decode is deliberately fail-closed on an unknown id. 47991 is 0xBB57, so byte 68 would read 0x57 = 87, and EVERY current client would fail the handshake against a host that had merely moved its mgmt port. `encode` therefore writes an explicit cipher byte whenever a port rides along (the placeholder discipline `Hello::encode` already uses); a current client reads AES, a pre-cipher client stops before 68 regardless. The test pins the byte, both offsets (69 AES / 101 ChaCha), and that a host advertising no port still emits exactly 68 bytes — this field costs the common case nothing. Host: `mgmt::effective_port()` reads the same resolved bind `publish_endpoint` writes, so the wire, the endpoint file and the mDNS TXT cannot disagree — one lookup, not a fourth place to compute a port. `0` on the standalone punktfunk1-host binary, which has no management API: advertising 47990 from a host that is not serving it would be worse than saying nothing. Clients persist it on connect, feeding the store plumbing fe2bfeca already built: * Rust — `on_connected` grew the port alongside the fingerprint, plus `learn_mgmt_port_by_fp` (keyed by fingerprint alone, the identity a just-connected client is certain of). * Apple — `PunktfunkConnection.hostMgmtPort` + `updateMgmtPort` at the existing markConnected site. * Android — a new `nativeHostMgmtPort` JNI call, persisted where the session is constructed. Verified: Linux (pf-lxcheck2, amd64) `cargo check --all-targets` clean across punktfunk-core, pf-host-config, punktfunk-host, pf-client-core, pf-presenter, punktfunk-cli, punktfunk-client-linux and punktfunk-client-session, each confirmed genuinely compiled (counting `Compiling` as well as `Checking` — cargo prints the former for bin-only packages, which is what made an earlier gate look vacuous when it was not). punktfunk-core quic tests 76/76. Android: :kit+:app Kotlin, ParseRecordTest 12/12, and cargoNdkClippy clean for aarch64-linux-android. Apple: xcframework rebuilt at ABI 20, `swift build` complete. cargo fmt --all --check clean. NOT verified: the Windows client (192.168.1.133 unreachable). |
||
|
|
99eb679c07 |
feat(clients): a moved mgmt port now outlives the advert that announced it
Moving the mgmt port off 47990 (the fix for sharing a box with a Sunshine fork, whose web UI owns
that port) only ever worked for as long as mDNS did. The real port lived in the advert and nowhere
else: every client read it live and threw it away, so on a VPN, a routed subnet, or any
multicast-dead network the library silently fell back to a port nothing was listening on.
`KnownHost` gains `mgmt_port: Option<u16>` + `effective_mgmt_port()` + `learn_mgmt_port()`, exactly
the shape `mac` and `os` already use ("learned from the advert while online, persisted so it
survives the host going to sleep") — except this one is load-bearing rather than cosmetic, so
`upsert` states the preserve rule explicitly instead of relying on the does-not-mention-it accident
that `clipboard_sync` survives by, and `upsert_trusted` carries it across a re-key.
Wired through all four client families, each of which was wrong in its own way:
* CLI / Windows / Linux reached for `DEFAULT_MGMT_PORT` at the call site — the constant is the
FALLBACK, not the answer. Windows also needed the port on `Target`, which the library screen has
instead of a `KnownHost`.
* The session console read `advert.and_then(mgmt_port)` with NO saved fallback, two lines above an
`os` that gets the three-rung treatment right. It now matches, and learns on every tick.
* Linux's `mgmt_port_for` consulted live adverts only; it now falls back to the store.
* Android never carried the port at all — its native discovery record stopped at 8 fields. Added
`mgmt` as the 9th (the record's own documented "new fields append, never reorder" rule), then
through `DiscoveredHost` -> `KnownHost` -> `LibraryScreen`.
* Apple LOOKED done and was not: `StoredHost.mgmtPort` and `effectiveMgmtPort` have existed all
along, but nothing anywhere wrote the field and the `mgmt` TXT was never parsed — so it was
permanently nil and every Apple client resolved to 47990 regardless. That is worse than the
honest omissions above, because it reads as finished. Now parsed, carried on `DiscoveredHost`,
and written by `HostStore.updateMgmtPort` at the same site that learns MACs and the OS chain.
Also `PUNKTFUNK_NATIVE_PORT` in host.env, finishing the pair with PUNKTFUNK_MGMT_BIND: `--native-port`
was likewise CLI-only and died on a package upgrade. A bad value is a startup ERROR rather than the
silent fall back to 9777 that `PUNKTFUNK_DATA_PORT` still does — the failure that reads as "I moved
the port and the client still can't reach me". The client side of the native port already worked
(`KnownHost.port` is persisted, `--connect HOST:PORT` names it).
Adding the field broke three `KnownHost` literals in tests, which is the `Default` impl's stated
purpose working ("adding a field here can't silently produce records that lack it"). All three now
carry 47991 — deliberately NOT the default, so the assertions cannot pass vacuously against a
hardcode. New coverage: forward-compat decode of a store predating the field, the resolver
fallback, re-key carry-forward, and on Android the 9th-field parse plus 0/non-numeric/out-of-range
all reading as unknown.
What this does NOT fix: a host that moved its mgmt port and has NEVER been seen over mDNS. Nothing
tells the client where to look, and the honest fix is for the host to announce it in-band — the
`Welcome` message has an established "append a trailing field, older peer decodes to the default"
pattern for exactly this, at the cost of a C ABI accessor and a bump. Left for a separate change.
Verified: Linux (punktfunk-rust-ci/pf-lxcheck2, amd64) `cargo check --all-targets` clean for
pf-host-config, punktfunk-host, pf-client-core, punktfunk-cli, punktfunk-client-linux and
punktfunk-client-session — the last confirmed non-vacuous by planting a compile_error! and watching
the gate fail (cargo prints "Compiling", not "Checking", for bin-only packages, so the usual marker
grep lies about it). Android: :kit + :app compileDebugKotlin clean, ParseRecordTest 12/12 with both
new cases named in the XML. Apple: xcframework built, `swift build` complete, SharedFoundationTests
pass. cargo fmt --all --check clean. NOT verified: the Windows client (192.168.1.133 unreachable).
|
||
|
|
bb78117504 |
feat(host): moving the management port off 47990 now survives, and the console follows
47990 is the management API's port and also Sunshine's (and Apollo's, and Vibeshine's) web UI port. With the GameStream planes off it is the ONLY port the two still share, so moving it is the whole of what "run both on one box" needs — except moving it was barely possible: * `--mgmt-bind` was the sole route, and it lives in a unit file / service registration that a package upgrade rewrites. There was no `host.env` key, so the change did not survive. * The literal 47990 appeared in SIX places — mgmt::DEFAULT_PORT, the Windows service's console launch, scripts/punktfunk-web.service, the NixOS module, web/web-run.cmd, and the console's own default. Nothing downstream could learn a different port, so moving the listener silently left the console proxying to a port nothing was listening on. Now there is one source of truth. `PUNKTFUNK_MGMT_BIND` joins `host.env` (the `--gamestream` / PUNKTFUNK_GAMESTREAM shape: either source works, the CLI flag wins), and `serve` publishes the port it ACTUALLY bound to ~/.config/punktfunk/mgmt-endpoint, in the same KEY=VALUE form mgmt-token already uses so it is sourceable as a systemd EnvironmentFile and readable by the Windows service's existing read_env_file_value. Every consumer derives from that; the 47990 literals survive only as the fallback that keeps an OLD host working with a NEW console. The two unit files drop their hardcoded `Environment=PUNKTFUNK_MGMT_URL=` rather than layering a default beneath the file: whether Environment= or EnvironmentFile= wins is a directive-ordering question, and the hand-written unit and the Nix-generated one do not order the same way. No default, no precedence puzzle — the server's own built-in fallback covers a host that never wrote the file. Two robustness details worth naming, because both fail in the same direction: * mgmt-endpoint is written write-then-rename. A torn read would set PUNKTFUNK_MGMT_URL to EMPTY, which is worse than a missing file — a built-in default only rescues an *unset* variable. * mgmtUrl() now treats blank as unset, which `??` alone does not. The publish happens in parse_serve next to the token persistence, so both files appear together; the console's unit gates on mgmt-token, and its Restart=always picks up a lost race anyway. What this does NOT change: a lost 47990 bind is still fatal to the whole host (the bind sits in tokio::try_join! with the native plane), and running two Moonlight-compatible hosts at once is still unsupported — on Windows the exclusive display topology is a second, independent conflict. Both are documented rather than altered. Verified on Linux in punktfunk-rust-ci (amd64): cargo check --all-targets clean for punktfunk-host and pf-host-config with the "Checking punktfunk-host" marker confirmed present (a first run exited 0 having compiled nothing — the warm shared target dir judged it fresh), 40/40 mgmt tests pass including the new one pinning the published line against both parsers that consume it. Console: tsc --noEmit clean, bun test server/ 9/9. cargo fmt --all --check clean. |
||
|
|
4676d20dc1 |
Host capture gain works on punktfunk/1, and boosting no longer hard-clips (#229)
arch / build-publish (push) Failing after 7m4s
apple / distribute (push) Successful in 11m50s
ci / docs-site (push) Successful in 9m13s
android / android (push) Successful in 8m2s
apple / screenshots (push) Successful in 8m39s
deb / build-publish-gamescope (push) Successful in 28s
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 12s
docker / builders-arm64cross (push) Failing after 8s
flatpak / build-publish (push) Successful in 11m42s
deb / build-publish-host (push) Successful in 5m14s
docker / deploy-docs (push) Failing after 4s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 27s
deb / build-publish (push) Successful in 12m23s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 13s
apple / swift (push) Successful in 1m56s
docker / builders (ci/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (push) Successful in 10s
deb / smoke-install (push) Failing after 5s
deb / build-publish-client-arm64 (push) Successful in 1m23s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 24s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 6m50s
windows-host / package (push) Failing after 12m55s
windows-host / canary-manifest (push) Skipped
windows-host / winget-source (push) Skipped
ci / web (push) Successful in 1m10s
ci / rust-arm64 (push) Failing after 2m16s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m2s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m1s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m19s
ci / rust (push) Successful in 28m39s
ci / bun-nix (push) Successful in 18s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m58s
|
||
|
|
be0030f953 |
Merge pull request #228 from worktree-azure-trusted-signing
android / android (push) Canceled after 0s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Canceled after 0s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (push) Canceled after 0s
apple / swift (push) Canceled after 0s
apple / distribute (push) Canceled after 0s
windows-host / package (push) Canceled after 0s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 0s
ci / rust (push) Canceled after 0s
ci / rust-arm64 (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bun-nix (push) Canceled after 0s
deb / build-publish (push) Canceled after 0s
deb / build-publish-host (push) Canceled after 0s
deb / build-publish-gamescope (push) Canceled after 0s
decky / build-publish (push) Successful in 1m0s
deb / build-publish-client-arm64 (push) Canceled after 0s
deb / smoke-install (push) Canceled after 0s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 0s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 0s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Canceled after 0s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / builders (ci/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 0s
Sign Windows releases with Azure Artifact Signing — a 3-day leaf makes timestamping mandatory |
||
|
|
cc70c64797 |
feat(screenshots): the add-host sheet becomes a scene — the blends' phone screens were three designs old
The Blender store scenes render whatever screens/ holds, and theirs were June captures of the pre-console UI. Fresh captures existed for hosts and pair but the add-host sheet had no scene: AddHostSheet's state is hoisted (ConnectScreen keeps half-typed values across dismissal), so the scene passes a filled form straight in. Two capture-truth fixes with it: dialog scenes advance the frozen clock 1.6 s (a ModalBottomSheet's entrance spring is still mid-rise at 0.8 s), and the add-host shot uses Pixel-like geometry (411×915dp @ 420 dpi — same 1080×2400 px, but the dp headroom is what lets the Connect button, the row carrying the resolution promise, fit in frame). |
||
|
|
8ee963b2b0 |
Merge pull request 'Exclusive topology left the KDE panel lit under a gamescope spawn — DPMS it dark' (#227) from worktree-gamescope-exclusive-dpms into main
docker / builders (ci/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (push) Failing after 2s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Failing after 1s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Failing after 2s
ci / rust-arm64 (push) Failing after 3s
docker / builders-arm64cross (push) Skipped
ci / rust (push) Canceled after 5m33s
ci / bun-nix (push) Successful in 1m29s
ci / web (push) Successful in 3m32s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Failing after 2s
deb / build-publish-host (push) Failing after 2m10s
ci / docs-site (push) Successful in 4m13s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Failing after 8s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Failing after 2s
docker / deploy-docs (push) Skipped
deb / build-publish (push) Canceled after 5m32s
deb / smoke-install (push) Canceled after 0s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Failing after 24s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Failing after 22s
android / android (push) Canceled after 5m25s
deb / build-publish-gamescope (push) Failing after 1m50s
deb / build-publish-client-arm64 (push) Failing after 1m40s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 15s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 12s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Failing after 6s
windows-host / package (push) Canceled after 5m46s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
arch / build-publish (push) Canceled after 5m30s
|
||
|
|
2d15548e38 |
ci(windows): provision the signing toolchain — no .NET runtime meant signtool exited 3 in silence
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 7m59s
android / android (pull_request) Failing after 2m16s
ci / web (pull_request) Successful in 2m29s
ci / rust-arm64 (pull_request) Successful in 2m35s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 4m24s
apple / swift (pull_request) Successful in 2m7s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / bun-nix (pull_request) Successful in 5m25s
ci / rust (pull_request) Successful in 7m16s
ci / docs-site (pull_request) Successful in 7m48s
Verified the whole Azure signing path on the runner (.133) today and it failed twice, for two reasons that neither error message named. Both are now provisioned here so a rebuild from the unom/infra Packer template cannot silently un-fix them. Azure.CodeSigning.Dlib.dll is a mixed-mode C++/CLI assembly: it ships Ijwhost.dll and a runtimeconfig.json pinning Microsoft.NETCore.App 8.0.0. The runner had NO .NET runtime at all — pwsh 7 is a self-contained install and brings no shared runtime — so signtool exited 3 having printed absolutely nothing. Installing the .NET 8 runtime turned that into a clean sign. The client itself installs machine-wide under C:\trusted-signing rather than a user's .nuget, because act_runner runs as SYSTEM, whose USERPROFILE is C:\Windows\System32\config\systemprofile. A per-user install under Administrator is invisible to every job that actually builds. Confirmed by resolving Find-AzureDlib from a SYSTEM scheduled task, which is also how the earlier SSH-only attempts misled: over a network logon New-SelfSignedCertificate hits NTE_PERM, so a control test that "fails" there proves nothing about how CI will behave. Both downloads are SHA-256 pinned against version-immutable URLs (nuget.org flat-container and the dotnet builds CDN), so they fail closed on tampering rather than on every Microsoft patch release — unlike the BtbN `latest` pin above, which re-rolls. The .NET install uses Start-Process -Wait because the bundle is a GUI PE that returns instantly under `&`, leaving $LASTEXITCODE unset and racing the completion check (cost one false failure here). End-to-end result on .133, as SYSTEM: sign rc=0, verify rc=0, chain Microsoft Identity Verification Root CA 2020 -> ID Verified CS EOC CA 04 -> "unom - Enrico Buhler", leaf thumbprint DD6A610F242CB5B2078C2A5D628699B6AB0CAC07 (matches the profile Azure reports), timestamped, leaf expires in 3 days as expected. Signing an unsigned binary and reading the subject back reproduces pack-msix.ps1's Publisher assertion exactly (match=True) — checked against a NON-catalog-signed binary on purpose, because Get-AuthenticodeSignature on a catalog-signed system exe returns the catalog signer and would have read as a false mismatch. |
||
|
|
6eb5edaff4 |
feat(audio): capture gain on punktfunk/1, and a soft knee instead of the clamp that made boosting a trap
android / android (pull_request) Failing after 1m5s
apple / swift (pull_request) Successful in 1m57s
apple / distribute (pull_request) Skipped
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m37s
ci / web (pull_request) Successful in 1m58s
ci / bun-nix (pull_request) Successful in 41s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 6m41s
ci / rust (pull_request) Successful in 21m14s
ci / rust-arm64 (pull_request) Failing after 3m56s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 2m50s
`PUNKTFUNK_AUDIO_GAIN` had two defects that compounded. It existed only on the GameStream plane, so on native `punktfunk/1` it silently did nothing — and since WASAPI loopback is tapped UPSTREAM of the endpoint's master volume, turning the host's speaker slider up does not change the level a client receives either. Between the two there was no host-side way at all to lift a quiet desktop mix on the protocol that matters. And where it did apply it was `(s * gain).clamp(-1.0, 1.0)` — a hard clip. Flat-topping a waveform is a first-derivative discontinuity, which radiates harsh high-order harmonics, so any operator who pushed past roughly 1.5x heard gross distortion long before reaching the level they were chasing. A field report of "+18 dB and everything warbles" is the expected output of that line, not a fault anywhere downstream of it. `punktfunk_core::audio::apply_gain` replaces the clamp with a tanh soft knee above 0.7 (~-3.1 dBFS), chosen for three properties: C1-continuous where the branches meet (slope 1 on both sides, so the onset of limiting is not itself an audible event), bounded by construction (asymptotic to 1.0, and +-inf maps to +-1.0, so nothing leaves out of range), and odd-symmetric (benign harmonics, no DC). It is a memoryless waveshaper, so it costs zero latency in the realtime encode path. Unity is a no-op inside `apply_gain` itself, not merely at the call sites, so the default wire stays byte-for-byte identical and a future caller that forgets to gate cannot quietly bend every peak. `capture_gain` is now shared by both planes and rejects the two values that are always typos: non-positive (would invert or mute) and above 8.0/+18 dB (capped, and said out loud). This buys headroom, NOT loudness. It cannot close a peak-to-loudness gap against already-limited broadcast content; that needs a compressor with a real time constant, which this deliberately is not, and the docs say so. `SOFT_LIMIT_KNEE` is excluded from cbindgen: it is host-side capture processing that no C embedder can act on, and exporting it would add a bare `#define` against the config's own R21 rule. Verified by regenerating `include/punktfunk_core.h` — byte-identical, ABI 19 untouched. |
||
|
|
9dde564835 |
Merge pull request 'CRA Phase 1 closeout — vendor-CVE watch doc, retention verified, docs-site deps current' (#226) from worktree-cra-phase1-closeout into main
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 8s
docker / builders (ci/gamescope-trixie.Dockerfile, punktfunk-gamescope-trixie) (push) Successful in 14s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 14s
ci / docs-site (push) Successful in 1m9s
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 10s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 15s
docker / builders-arm64cross (push) Successful in 17s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m10s
docker / deploy-docs (push) Successful in 35s
ci / web (push) Successful in 5m55s
audit / c-abi-asan (push) Successful in 9m42s
audit / bun-audit (plugin-kit) (push) Successful in 2m7s
audit / bun-audit (sdk) (push) Successful in 14s
audit / bun-audit (web) (push) Successful in 15s
audit / docs-site-audit (push) Successful in 16s
audit / pnpm-audit (push) Successful in 9s
nix / flake (push) Successful in 13m53s
audit / cargo-audit (push) Successful in 5m42s
ci / rust (push) Successful in 20m6s
ci / rust-arm64 (push) Successful in 1m17s
audit / license-gate (push) Successful in 4m29s
audit / miri (push) Successful in 4m36s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 11s
ci / bun-nix (push) Successful in 22s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 8s
|
||
|
|
b79ff45bd1 |
feat(windows): sign via Azure Artifact Signing — a 3-day leaf makes timestamping mandatory
Releases move from the self-signed CN=unom cert to Azure Artifact Signing (formerly Trusted Signing): account `unomsigning`, profile `unom-io`, signed by the `punktfunk-ci-signing` service principal, which holds only the Artifact Signing Certificate Profile Signer role scoped to that one profile. Both pack scripts gain the backend ahead of the existing .pfx and ephemeral fallbacks, so canary and fork builds are unaffected. Three things that are easy to get wrong, and are handled here rather than discovered in the field: Azure mints a leaf certificate per signing request that expires in about three days. Both scripts previously retried WITHOUT a timestamp when a timestamped sign failed — under Azure that ships an artifact which verifies on the runner and goes untrusted days later, on every user's machine at once. The retry is now gated on the mode: still lenient for a .pfx whose cert outlives the release, a hard failure for Azure. The MSIX manifest Publisher must equal the signer subject byte-for-byte, because package identity is Name + Publisher. The default is now the profile's verified subject, written with `[char]0xFC` escapes rather than literal umlauts so this UTF-8-without-BOM file cannot silently mojibake the DN into one that no longer matches. pack-msix.ps1 now also reads the signature back off the packed .msix and fails on drift — asymmetric on purpose: a subject that disagrees is fatal, a subject that cannot be read is only a warning, since Get-AuthenticodeSignature's .msix support varies by Windows version and signtool has already reported success by then. NOTE this changes package identity, so existing installs need an uninstall, not an upgrade. The updater's leaf-pinning note was wrong and is corrected: update/windows.rs claimed the AUTHENTICODE_SHA256 field made Trusted Signing "a manifest edit", but a per-request leaf is exactly what a leaf pin cannot track — a pin would go stale within days and reject every release after it. Drivers are deliberately untouched: their catalogs keep the DRIVER_CERT_* cert and the installer still plants it as a machine root. The two signatures were always independent (SmartScreen/UAC vs PnP), which is why the installer could move without them. Whether a publicly-trusted catalog would let us drop that root plant is recorded as an unverified follow-up, not assumed. Verified: both scripts parse under the PowerShell 7 AST parser, both workflows are valid YAML, the evaluated Publisher default matches the subject Azure reports for the profile (86 chars, ordinal), rustfmt clean. NOT verified on Windows — the sign path itself needs an on-glass run on .133. |
||
|
|
b66bcef528 |
fix(screenshots): the macOS leg built the harness out of existence
The whole shot harness is #if DEBUG, and shoot_macos built -c release — so the binary launched as the NORMAL app, never printed PF_SHOT_WINDOW, and every scene 'never reported a window' while the script SIGKILLed a perfectly healthy app. Build debug: SwiftUI has no release-only visuals, and the harness actually exists there. All eight mac scenes capture now. |
||
|
|
42848c56b7 |
docs(compliance): the vendor-CVE watch the technical file will cite — and the SBOM learns we ship Bun
ci / rust-arm64 (pull_request) Successful in 1m33s
ci / bun-nix (pull_request) Successful in 1m33s
ci / web (pull_request) Successful in 2m5s
ci / docs-site (pull_request) Successful in 2m20s
ci / rust (pull_request) Successful in 5m58s
nix / flake (pull_request) Successful in 14m59s
compliance/vendored-components.md records, per vendored/bundled component, where the pin lives, how it updates, and which feed to watch — the CRA Art. 13(5) due-diligence evidence (S4 in the roadmap). Retention verified while writing it: Gitea serves the full release history v0.17.x -> current, stable sysext feeds publish KEEP=0, flatpak rsyncs without --delete. The manual SBOM fragment gains the bundled Bun 1.3.14 runtime (portable bun.exe in the Windows installer for the console + plugin runner — it was in no lockfile and no SBOM) and stops hardcoding the gamescope patch count at 3 when the series is at 9. SECURITY.md gets the one sentence Annex I Part II asks for: security fixes are free, prompt, and ride patch releases — which the stable channel already did, unwritten. |
||
|
|
39b9e9e276 |
chore(docs-site): current deps all around — the 67 leftover advisories all live inside @unom/ui's payload chain
bun update (fumadocs 16.14, tanstack ~1.170, react 19.2) plus @unom/ui 0.8.16 -> 0.9.2 and @unom/app-ui 0.1 -> 0.2.1. Build, tsc --noEmit and a served smoke test all pass. The audit stays non-blocking: every remaining advisory is pinned inside @unom/ui's own dependency tree (@payloadcms/* -> fast-uri/ image-size/sharp, next 16.x, sass -> immutable) — nothing bumpable from this lockfile, and overrides would fork what the CMS actually ships. The comment in audit.yml now names that blocker instead of the stale dompurify/node-tar list. |
||
|
|
79114891df |
fix(vdisplay): exclusive topology left the KDE panel lit under a gamescope spawn — DPMS it dark
ci / bun-nix (pull_request) Successful in 1m42s
ci / web (pull_request) Successful in 3m41s
ci / rust (pull_request) Successful in 23m56s
ci / rust-arm64 (pull_request) Successful in 6m2s
android / android (pull_request) Successful in 7m12s
ci / docs-site (pull_request) Successful in 7m45s
A bare-spawn gamescope session is its own headless compositor, so it was the one Linux route that never consulted effective_topology(): on a KDE desktop box the physical panel kept showing the idle desktop for the whole stream while the policy said exclusive. The KWin route's mechanism (disable the physicals) is closed on this route — KWin refuses a configuration with zero enabled outputs and no output on that desktop is ours to leave enabled — so the honest translation is DPMS: the desktop stays exactly where it is, the panels go dark, local input wakes them, and stream input never does (it enters gamescope's own EIS socket, not KWin's libinput). New kwin_dpms module drives the vendored org_kde_kwin_dpms protocol in-process over the desktop's own Wayland (the kwin_output_mgmt stack and rationale), with a kscreen-doctor --dpms fallback on kwin.rs's shared verdict/budget. The darken is refcounted host-wide rather than floated through the registry's per-group restore, because every gamescope spawn is its own group — the float alone would re-light the panel when the first of two concurrent spawns ends. Each exclusive spawn registers the release as its per-display topology restore, so the registry still times every release (§6.1) and the last one out re-lights only what the first darken actually turned off. Crash-safe by construction: DPMS is non-persistent, so a dead host leaves nothing to journal — the panel re-lights on the next local input. Managed and Attach are deliberately untouched: managed's takeover already stopped the desktop, and attach may be mirroring a gamescope that is itself driving the physical panel. |