e5180a5b7dbee894ef381afba96bc2abd5fa7c1b
2832
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e5180a5b7d |
chore(release): bump workspace version to 0.26.0
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m13s
apple / swift (pull_request) Successful in 1m35s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 2m39s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m15s
ci / bun-nix (pull_request) Successful in 56s
ci / docs-site (pull_request) Successful in 3m39s
ci / rust-arm64 (pull_request) Successful in 7m12s
android / android (pull_request) Successful in 9m5s
ci / rust (pull_request) Successful in 20m42s
nix / flake (pull_request) Failing after 20m28s
47 commits since v0.25.0, most of them from field reports on 0.25.0 itself, plus Wave 2 of the PyroWave Linux host-performance program. Nothing breaks: the wire protocol stays at 2 and the C ABI stays at 17, so this release adds no call, no message and no capability bit. pf-driver-proto is byte-for-byte identical to v0.25.0 and to v0.24.0. Four new environment variables (PUNKTFUNK_OVERLAY_MASK, PUNKTFUNK_GAMESCOPE_REFRESH_RATES, PUNKTFUNK_PYROWAVE_CHUNK_KIB, PUNKTFUNK_PYROWAVE_STREAMED_AU), verified new by git grep at the v0.25.0 tag rather than assumed. plugin-kit goes 0.3.2 -> 0.4.0 for the `plugin` launch kind; the SDK goes 0.1.2 -> 0.1.4; gamescope patch level +pfhdr2 -> +pfhdr4. Two behaviour changes make a client advertise LESS than it used to, both deliberate: VIDEO_CAP_444 is now probed against the driver rather than ridden off the setting alone (every Steam Deck with "Full chroma" on was losing HEVC entirely, not crispness — no AMD silicon decodes HEVC 4:4:4), and the Decky client-update check now reports a failure instead of dressing it up as "up to date". Bump is the same four files as 0.25.0: Cargo.toml, Cargo.lock, docs/releases/v0.26.0.md, docs/releases/whatsnew/v0.26.0.txt — plus the CHANGELOG.md section, which the split at 0.25.0 made part of the ritual. Gates run locally, all green: * cargo fmt --all --check clean * cargo metadata --locked resolves * Cargo.lock diff versions-only, 70/70 changed lines, 35 crates * Play whatsnew gate 398/500 chars, not byte-identical to any other release * notes voice check 0 internal-vocabulary hits above "## For developers" Notes are 66 lines against 0.25.0's 83, covering 47 commits. Still owed on glass and recorded in the CHANGELOG's verification table: iPhone + Bluetooth listen, Apple TV stats overlay, MacBook audio listen, the Deck HEVC/4:4:4 retest, a Windows wake-from-sleep cycle, and the PyroWave-under-game-load A/B with CAP_SYS_NICE actually granted. |
||
|
|
4070d043d6 |
Merge pull request 'PyroWave on Linux: the GPU-priority knob never fired, a dmabuf timeout condemned the host, and the jumbo grow was dead code' (#132) from worktree-wave2-pyrowave into main
apple / swift (push) Successful in 1m35s
android / android (push) Canceled after 3m13s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 3m9s
ci / rust (push) Canceled after 2m42s
ci / rust-arm64 (push) Canceled after 1m10s
ci / web (push) Canceled after 3s
ci / docs-site (push) Canceled after 0s
ci / bun-nix (push) Canceled after 0s
deb / build-publish (push) Canceled after 2s
deb / build-publish-host (push) Canceled after 0s
deb / build-publish-client-arm64 (push) Canceled after 0s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 0s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 0s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Canceled after 0s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
flatpak / build-publish (push) Canceled after 8s
nix / flake (push) Canceled after 7s
release / apple (push) Canceled after 5m6s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 15s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 14s
windows-host / package (push) Canceled after 3m27s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Canceled after 0s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Canceled after 0s
windows / build (aarch64-pc-windows-msvc) (push) Canceled after 0s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
Reviewed-on: #132 |
||
|
|
ebf61cb448 |
Merge branch 'worktree-wave2-pw5-encode-overlap' into worktree-wave2-pyrowave
apple / swift (pull_request) Successful in 1m34s
apple / screenshots (pull_request) Skipped
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m9s
ci / bun-nix (pull_request) Successful in 1m19s
ci / docs-site (pull_request) Successful in 1m57s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m13s
ci / web (pull_request) Successful in 3m25s
android / android (pull_request) Successful in 5m4s
ci / rust-arm64 (pull_request) Successful in 6m29s
ci / rust (pull_request) Successful in 11m37s
nix / flake (pull_request) Successful in 18m17s
# Conflicts: # crates/pf-encode/src/enc/linux/pyrowave.rs |
||
|
|
4a92c64144 | Merge branch 'worktree-wave2-pw7a-jumbo-shard' into worktree-wave2-pyrowave | ||
|
|
2426056465 | Merge branch 'worktree-wave2-pw6-streamed-au' into worktree-wave2-pyrowave | ||
|
|
d3aaa16a7d |
Merge branch 'worktree-wave2-pw3-dmabuf-latch' into worktree-wave2-pyrowave
# Conflicts: # packaging/arch/punktfunk-host.install # scripts/steamdeck/install.sh |
||
|
|
2dd65bdd41 |
Merge pull request 'Opening the Steam menu on the Deck moved the game too — the pad is now held neutral while an overlay owns it' (#131) from worktree-deck-overlay-input-mask into main
apple / swift (push) Successful in 1m38s
audit / cargo-audit (push) Successful in 1m45s
audit / bun-audit (plugin-kit) (push) Successful in 37s
audit / bun-audit (sdk) (push) Successful in 30s
audit / bun-audit (web) (push) Failing after 26s
audit / docs-site-audit (push) Successful in 26s
audit / pnpm-audit (push) Successful in 11s
arch / build-publish (push) Successful in 9m2s
ci / rust-arm64 (push) Successful in 3m6s
android / android (push) Successful in 10m27s
audit / license-gate (push) Successful in 5m59s
ci / web (push) Successful in 1m30s
ci / bun-nix (push) Successful in 58s
ci / docs-site (push) Successful in 1m56s
release / apple (push) Successful in 9m51s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 1m5s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 1m52s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 12s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 31s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
windows-host / package (push) Successful in 12m31s
windows-host / winget-source (push) Skipped
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 13s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 19s
apple / screenshots (push) Successful in 5m48s
deb / build-publish-client-arm64 (push) Successful in 6m51s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m35s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m45s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m13s
deb / build-publish-host (push) Successful in 11m34s
ci / rust (push) Canceled after 18m48s
deb / build-publish (push) Canceled after 12m54s
docker / builders-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
flatpak / build-publish (push) Canceled after 8m0s
nix / flake (push) Canceled after 8m1s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 8m2s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 5m37s
windows-host / canary-manifest (push) Canceled after 0s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 1m30s
Reviewed-on: #131 |
||
|
|
5cbaca7789 |
feat(client/pads): stop forwarding the pad while the Steam overlay owns it
ci / bun-nix (pull_request) Successful in 32s
ci / docs-site (pull_request) Successful in 1m21s
ci / web (pull_request) Successful in 1m37s
apple / swift (pull_request) Successful in 1m41s
apple / screenshots (pull_request) Skipped
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m19s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m37s
ci / rust-arm64 (pull_request) Successful in 5m50s
android / android (pull_request) Successful in 7m56s
ci / rust (pull_request) Successful in 15m55s
nix / flake (pull_request) Failing after 16m33s
On a Deck in Gaming Mode the Steam menu and the QAM are driven by the SAME physical controller the client forwards, so opening either one moved the game on the host as well as Steam's UI — a second, invisible player. Steam Input masks a normal game here; it cannot mask us, because masking happens on Steam Input's virtual pad and we deliberately forward the REAL one (28DE:1205 — the virtual pad has no gyro, trackpads or paddles). SDL ships the exact behaviour we want and it is on by default: presses are dropped while the process has windows but no keyboard focus, releases still get through. It CANNOT fire on a Deck. gamescope resolves focus per Xwayland ctx and the client sits alone in its own, so the Steam overlay — which lives in the root ctx — never takes our X focus away and no FocusOut is ever generated. Measured on glass: with the QAM open, X input focus inside the client's ctx stayed on its window for the whole 4 s, while GAMESCOPE_FOCUSED_APP flipped to 769 (Steam) and GAMESCOPE_FOCUSED_APP_GFX stayed on the app. So the signal is explicit. `overlay_focus` watches those two atoms on the gamescope root ctx — which is NOT our own $DISPLAY under `--xwayland-count 2`, hence the socket-directory walk and the flatpak filesystem line — and the presenter ORs it with window focus into one `set_masked`. Masking is deliberately not `set_forwarding`: that closes the slot and sends GamepadRemove, so the game would see a controller UNPLUG every time somebody opened the QAM. This keeps every slot open and only stops the transitions, after flushing what the host believes is held so a stick deflected at overlay-open stops steering instead of freezing at its last value. On the way back, held buttons are adopted rather than replayed — the A that picked a QAM row must not fire in the game as it closes — while axes are re-sent, since a stick has no press to ghost and SDL only speaks on change. Fails open throughout: no gamescope, no X, or an unreadable signal all leave forwarding exactly as it was. `PUNKTFUNK_OVERLAY_MASK=0` opts out. |
||
|
|
9c854893bc |
Merge pull request 'plugin-kit 0.4.0 — publish the launch surface #129 added, because rom-manager's main is red until it exists' (#130) from worktree-plugin-kit-040 into main
ci / web (push) Canceled after 0s
ci / docs-site (push) Canceled after 0s
ci / bun-nix (push) Canceled after 0s
ci / rust (push) Canceled after 7s
ci / rust-arm64 (push) Canceled after 2s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Canceled after 1s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 0s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Canceled after 0s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
docker / builders-arm64cross (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
nix / flake (push) Canceled after 2s
plugin-kit-publish / publish (push) Successful in 39s
Reviewed-on: #130plugin-kit-v0.4.0 |
||
|
|
6b7997cace |
chore(plugin-kit): 0.4.0 — the launch surface a plugin needs to publish a tile the host cannot name
ci / docs-site (pull_request) Canceled after 30s
ci / web (pull_request) Canceled after 45s
ci / rust (pull_request) Canceled after 3m3s
ci / rust-arm64 (pull_request) Canceled after 1m52s
ci / bun-nix (pull_request) Canceled after 0s
nix / flake (pull_request) Canceled after 5s
`serveUi({launch})`, `PluginLaunchTarget` and `makeLaunchHandler` (#129) are new API, so this is a
minor bump rather than a patch. It also carries `SyncError.message`, without which a host refusal
reaches a plugin's own UI as the bare tag `SyncError` and nothing else.
Unblocks rom-manager, whose main is currently RED: it merged the consuming change while still
pinning `^0.2.0`, so `bun install --frozen-lockfile` there resolves a kit without these exports and
the typecheck fails on all three. Publishing this and then bumping that pin is the fix — in that
order, because the lockfile cannot resolve 0.4.0 until it exists on the registry.
Tag `plugin-kit-v0.4.0` to publish; the workflow asserts the tag matches this version.
|
||
|
|
78ba2342b5 |
Merge pull request 'rom-manager has been putting 0 games in the library since 08-05 — a plugin launch kind, so a scanner can publish tiles the host cannot name' (#129) from worktree-rom-manager-plugin-launch into main
apple / swift (push) Successful in 1m39s
ci / web (push) Successful in 1m45s
ci / docs-site (push) Successful in 1m35s
ci / bun-nix (push) Successful in 42s
ci / rust-arm64 (push) Successful in 2m45s
deb / build-publish-client-arm64 (push) Successful in 1m4s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 7s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 6s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 9s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
deb / build-publish (push) Successful in 3m55s
android / android (push) Successful in 6m31s
docker / builders-arm64cross (push) Successful in 6s
docker / deploy-docs (push) Successful in 30s
apple / screenshots (push) Successful in 6m8s
ci / rust (push) Canceled after 8m47s
deb / build-publish-host (push) Successful in 8m5s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 5m54s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 4m57s
windows-host / package (push) Successful in 11m52s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 17s
arch / build-publish (push) Successful in 13m1s
Reviewed-on: #129 |
||
|
|
7d37fe450d |
test(pf-encode): run PyroWave at depth 2 on real hardware — without shipping depth 2
Wave-2 PW5, the stage-6 experiment. Shipped behaviour is UNCHANGED: `max_inflight` is still 1. Stage 6 is the frame-corruption stage, and its gate is an on-glass tear-hunt with a live compositor, a real client and ten minutes of moving content. That is not runnable from here. But the depth-2 risk has two halves, and one of them lives entirely in this crate — the per-slot resources (`cmd`/`fence`/`csc_set`/y/uv/cursor) and the alternating encoder handles — so that half can be answered now, on the GPU, and the answer is worth having before anyone attempts the other. The experiment drives the backend with two frames genuinely in flight (submit N+1, then poll N) and compares the result against the encoder's OWN synchronous output over the same 16 moving frames. Its own depth-1 decode is the honest reference: pyrowave's raw AU bytes are not reproducible run-to-run (see the stage-3 commit), but its decoded planes are. RESULT, .21 / RTX 5070 Ti (GPU idle at 180 MHz of 3090 — the slow-clock worst case on this card): depth-2 vs depth-1 over 16 frames: worst-case PSNR identical (inf) Bit-identical luma, every frame, in order. So stages 4 and 5 between them are sufficient for the encoder side: doubling the six single-slot resources and alternating two `pyrowave_encoder` handles under one monotonic wire sequence really does make overlap invisible to the decoder. The test is built to fail rather than to pass. Content MOVES every frame (flat fills are the documented false-green trap — a torn frame stitched from two halves of a static card is invisible), it asserts two frames were ACTUALLY in flight rather than silently proving nothing, it asserts the AU count is unchanged, and it carries an off-by-one discriminator that raw PSNR would miss: each overlapped frame must match its own reference BETTER than it matches the previous one, so a pipeline delivering frames one position late fails even though every individual PSNR looks fine. It reaches `max_inflight` directly instead of through a shipped knob, precisely so the shipped value stays 1. ⚠ WHAT THIS DOES NOT COVER, stated here so the next person does not read it as a green light for stage 6: the CAPTURE side. `.process` hands the SPA buffer back to the compositor at callback return while the encode thread holds only a dup of its dmabuf fd, so a second frame in flight widens the window in which the producer may overwrite a buffer we are still reading by a full frame period. Nothing in this crate can test that — it needs a live producer. Stages 1 and 2 are what make it answerable (the pool census says how deep the producer's ring is; the Choice range asks for headroom), and the on-glass hunt is what would settle it. Gates green at CI parity. |
||
|
|
077db416ec |
feat(pf-encode): two PyroWave encoder handles, and the 3-bit landmine that makes them work
Wave-2 PW5 stage 5. Depth is STILL 1 — the handles alternate per frame, one in flight. PyroWave's `Encoder` cannot hold two frames. Not "probably not" — structurally not. `Encoder::Impl` owns ONE each of `wavelet_img_high_res`, `bucket_buffer`, `meta_buffer`, `block_stat_buffer`, `payload_data` and `quant_buffer`, and `Impl::encode` OPENS by discarding them: an image barrier with `VK_IMAGE_LAYOUT_UNDEFINED` as the old layout — a written promise that nothing else is reading it — plus three `fill_buffer` clears. Two encodes recorded into two command buffers and submitted to one queue have no execution dependency in Vulkan (submission order orders the START, not the completion), so N+1's DWT would overwrite the wavelet bands and zero the RDO buckets while N's block packing still reads them. Content-dependent, silent. So overlap means TWO handles on one device, alternated — one per slot. Every resource above is then private per handle, and within a handle the encodes stay strictly serialized (a slot's next frame is recorded only after that slot's previous one retired), which leaves patch 0004's scratch-pool invariant intact without touching it. THE LANDMINE, and it is the reason this stage is its own commit: `sequence_count` ALSO lives on `Impl`, and it is the 3-bit counter stamped into every block header. Two handles each count 1,2,3... alone, so the wire sees 1,1,2,2,3,3.... The decoder restarts a frame only when the value CHANGES (`diff = (hdr.sequence - last_seq) & 0x7; restart = diff != 0`), so a repeat reads as MORE BLOCKS OF THE SAME FRAME: `clear()` never runs, `decoded_frame_for_current_sequence` stays true, and the second frame of each pair is swallowed. Half frame rate, occasional mixed-frame blocks, no error anywhere — on every client, since pf-client-core and the Apple Metal hand-port parse the same field. `patches/0007-encoder-sequence-override.patch` (new, ~38 lines) exposes `Encoder::set_next_sequence` + a `pyrowave_encoder_set_next_sequence` C entry + a `PYROWAVE_SEQUENCE_MASK` define, so ONE monotonic counter on the Rust side is stamped regardless of which handle encodes. The setter stores `(seq - 1) & mask` because `Impl::encode` pre-increments — its contract is about the next ENCODE, not the next store. Inert when unused, so the whole Windows backend is untouched. No `.def` change: the C API is a static archive. PREDICTED, THEN OBSERVED. A negative control on .21 (the override call removed, nothing else) reads the wire out at exactly: [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 0, 0, 1, 1, 2, 2] which is the analysis's prediction character for character, and with the override: +1 mod 8, all 20 frames, through the 3-bit wrap. THE GATE, `wire_sequence_increments_across_alternating_handles`, checks three things over 20 frames because any one alone could pass while the stream is broken: the wire counter advances by 1 mod 8; ONE persistent decoder (its `last_seq` carried across every push, exactly like a client's) reports every AU decodable; and consecutive decoded pictures DIFFER. Content moves every frame — and the first run caught a trap in the harness itself rather than the encoder: `test_card` starts its LCG at `seed | 1`, so seeds 2 and 3 build a byte-identical card and the test faked the very repeat it hunts. Odd seeds only now, with the reason written down. A runtime self-check backs the test up where the test cannot reach: after packetize, the stamped sequence is compared against what we asked for, and a mismatch logs once per process naming patch 0007. A re-vendor that loses the patch would not fail to build — it would fail on glass, subtly, and this makes it loud instead. Two byte reads per frame. `reset()` rebuilds both handles and `Drop` destroys both, each with the same null-immediately discipline the single handle had (`pyrowave_encoder_destroy` is a bare `delete` with no null check, so a stale pointer left in the field is a double free). Vendored-patch discipline: patch 0007 re-applies clean to a pristine vendor checkout (verified by stashing the vendor tree and re-applying), and `git diff crates/pyrowave-sys/vendor/` touches exactly the four intended files. VERIFIED ON GLASS (.21, RTX 5070 Ti, GPU idle at 180 MHz of 3090): all 8 `#[ignore]`d GPU tests pass, including the new gate and the 4:2:0 / 4:4:4 / 24-bpp PSNR smokes. Gates green at CI parity. |
||
|
|
fd98406868 |
Merge pull request 'The Decky plugin's "update the client" has never once detected an update' (#128) from worktree-decky-client-update into main
ci / web (push) Successful in 1m54s
ci / rust-arm64 (push) Successful in 3m56s
ci / bun-nix (push) Successful in 29s
ci / docs-site (push) Successful in 1m32s
decky / build-publish (push) Successful in 30s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 12s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 8s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 10s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 17s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 15s
ci / rust (push) Successful in 8m12s
docker / builders-arm64cross (push) Successful in 20s
docker / deploy-docs (push) Successful in 46s
Reviewed-on: #128 |
||
|
|
5c70a90358 |
Merge pull request 'A Steam Deck could lose HEVC entirely to a chroma switch nothing checked — and the tool you'd triage it with denied the queue it was decoding on' (#127) from worktree-deck-hevc-shape-gates into main
ci / rust (push) Canceled after 25s
ci / rust-arm64 (push) Canceled after 25s
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/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 1m36s
deb / build-publish-client-arm64 (push) Successful in 2m8s
windows-msix / package (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m44s
windows-msix / package (x64, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m51s
deb / build-publish (push) Successful in 5m35s
deb / build-publish-host (push) Successful in 6m5s
apple / screenshots (push) Successful in 6m15s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m20s
android / android (push) Successful in 9m35s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m23s
arch / build-publish (push) Successful in 12m29s
flatpak / build-publish (push) Successful in 10m3s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m52s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 18m7s
Reviewed-on: #127 |
||
|
|
29248dcab9 |
feat(pf-encode): PyroWave had six single-slot resources, not the two the plan named
Wave-2 PW5 stage 4. Pure capacity — `max_inflight` is STILL 1, nothing overlaps yet.
The plan named the y/uv images as the thing to double. Reading the backend found five more, and
each is a correctness problem under overlap rather than a performance one:
* `csc_set` — ONE descriptor set, rewritten every frame by `bind_rgb`. Updating a set still bound
by a PENDING command buffer violates VUID-vkUpdateDescriptorSets-None-03047, and on most
drivers that is a wrong picture rather than an error.
* `y_img`/`uv_img` — the CSC of N+1 storage-writes exactly the images pyrowave is still sampling
for N. The barrier comment ("the previous frame's encode already completed under our
synchronous fence") was load-bearing and said so.
* `cursor_img` + `cursor_stage` — the struct comment stated the assumption outright: *"Single
(not ring) because PyroWave encodes one frame synchronously — no in-flight overlap to race."*
* `cmd` + `fence` — you cannot record into a PENDING command buffer at all.
* `cpu_img`/`cpu_stage` (software capture / tests) — the host writes staging while the previous
frame's copy is still pending.
All of it moves into a `Slot`, and the encoder now owns `SLOTS` of them. Two, because Granite caps
the overlap at two for us: the pyrowave device defaults to `init_frame_contexts(2)` and
`next_frame_context()` — called at the top of every `encode_gpu_synchronous` — waits the context it
rotates into. A third slot would need a vendored `init_frame_contexts(3)` that is not exposed.
`bitstream` and `import_cache` are deliberately NOT per-slot, and the `Slot` doc says why so a
later sweep does not "fix" it: `bitstream` is only touched during packetize, i.e. only on the poll
side one frame at a time, and `import_cache` retaining the VkImage/VkDeviceMemory per dmabuf inode
is precisely what makes it safe for two slots to sample the same imported buffer. `cpu_expand` is
shared for the same reason — it is copied into staging before `submit_frame` returns, so no GPU
work ever reads it.
Each frame carries its slot index in `InFlight` rather than recomputing it, so `wait_and_packetize`
cannot wait the wrong fence — the failure that would look like corruption rather than an error.
`reset()` now waits EVERY in-flight fence, not just one, which matters the moment depth rises.
WHAT IT COSTS, measured from the driver's own memory requirements rather than estimated (.21,
RTX 5070 Ti, and there is now an `#[ignore]`d test that prints it on any GPU):
1080p 4:2:0 3872 KiB per slot 7744 KiB for both
4K 4:2:0 12992 KiB per slot 25984 KiB for both
4K 4:4:4 24992 KiB per slot 49984 KiB for both
So the extra slot costs ~3.8 MiB at 1080p and ~24 MiB at 4K 4:4:4 — an order of magnitude under
the plan's ~25-35 MB / 100-150 MB estimate, because that estimate included pyrowave's internal
wavelet and scratch buffers, which stage 5's second encoder handle will add and this stage does
not. Affordable on an iGPU. The open line now logs `slots`, `slot_kib` and `slots_kib` so this is
visible per session and not only in a test.
VERIFIED ON GLASS (.21, GPU idle at 195 MHz of 3090 — slow-clock, the worst case on this card):
all 6 `#[ignore]`d GPU tests pass, and all NINE decoded-plane hashes (`ref-dense-{y,cb,cr}`,
`ref-chunked-*`, `ref-dense444-*`) are bit-identical to the pre-PW5 base. Decode identity is the
meaningful gate here — the raw AU bytes are not reproducible run-to-run even from an unmodified
binary, which stage 3's message documents.
Gates green at CI parity.
|
||
|
|
95962f55d0 |
refactor(pf-encode): PyroWave waited its fence inside submit — the one backend that did
Wave-2 PW5 stage 3. Depth is STILL 1; this is the shape change alone.
`encode_frame` recorded CSC+encode, queue-submitted, waited the fence and packetized, all inside
`Encoder::submit`. Every other backend in this crate puts the wait on the POLL side. That
difference is the whole reason the host loop's cadence folds around this encoder: with the wait
inline, `submit` returns only after the GPU is done, so the arrival-anchored floor absorbs the
encode only while it stays under 0.9x the frame interval.
Split into `submit_frame` (ingest -> CSC -> pyrowave encode -> queue-submit -> return) and
`wait_and_packetize` (fence wait -> packetize -> AU), with an `InFlight` deque between them capped
by `max_inflight`, which is 1. **One is the only value the resources can support today** — `cmd`,
`fence`, `csc_set` and the y/uv images are one each, so a second concurrent frame would record into
a PENDING command buffer and storage-write images pyrowave is still sampling. `submit` therefore
drains to `max_inflight - 1` before recording, which states that invariant in one place instead of
leaving it implicit in "the encode is synchronous".
The subtle part is the command-buffer state machine, and it is unchanged: the record-and-submit
closure still resets `cmd` on every PRE-submit failure (RECORDING/INVALID/EXECUTABLE, never
PENDING), and the fence wait still does NOT reset on failure, because a timeout leaves the buffer
PENDING where a reset violates VUID-vkResetCommandBuffer-commandBuffer-00045. What changed is that
a failed wait now also leaves the entry IN FLIGHT — which is precisely what tells `reset()` there
is live GPU work to re-wait before the pyrowave encoder object may be destroyed. `gpu_pending` is
gone; `!inflight.is_empty()` is the same fact, and cannot drift from it.
The split opened two windows that did not exist when everything ran inline, both closed here:
`reconfigure_bitrate` and `set_wire_chunking` can now land BETWEEN a submit and its poll, so the
packetize boundary and the bitstream cap are snapshotted into `InFlight` at submit time. Reading
the live fields would have let a mid-flight bitrate drop turn a perfectly good frame into
"unexpected packet count", and a mid-flight chunking change into an AU with the wrong
`chunk_aligned` flag.
`flush()` is no longer a no-op — it drains the in-flight frame, so the trait's poll-until-None
contract still returns every AU (the `spike` subcommand and the hardware smoke tests are the real
users).
The perf instrument still measures submit->AU, stamped at submit and taken when the AU becomes
readable, so `92326312`'s numbers stay directly comparable; the log line now carries `depth` and
says plainly that above depth 1 the number legitimately grows by about one loop period.
VERIFIED ON GLASS (.21, RTX 5070 Ti, GPU idle at 180 MHz of 3090 — so these are slow-clock runs,
which is the worst case on this card, not the best): all 6 `#[ignore]`d GPU tests pass — the
4:2:0, 4:4:4 and 24-bpp PSNR smokes, the mode-mismatch refusal, the fd-leak check and the golden
dump.
Byte-identity, honestly: the AU bytes are NOT reproducible, and were not before this commit
either. Three runs of the SAME unmodified binary produced three different `au-dense.bin` hashes
(ab7ecaf6 / 8735700e / 933b3d40) — the vendored 4:2:0 encoder emits run-varying bytes that the
decoder ignores. So the meaningful gate is DECODE identity, and that holds exactly: every decoded
plane (`ref-dense-{y,cb,cr}`, `ref-chunked-{y,cb,cr}`, `ref-dense444-{y,cb,cr}`) is bit-identical
between the pre-split base and this commit, across four runs. 4:4:4 AUs are additionally
bit-stable and match the checked-in Apple fixture exactly.
Gates green at CI parity.
|
||
|
|
9e598f8595 |
fix(client): the 4:4:4 switch could cost a Deck its whole codec, and --probe-decode denied the queue it was decoding on
ci / bun-nix (pull_request) Successful in 41s
ci / web (pull_request) Successful in 1m17s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m21s
apple / swift (pull_request) Successful in 1m35s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m37s
ci / rust-arm64 (pull_request) Successful in 3m39s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m22s
android / android (pull_request) Successful in 3m56s
ci / rust (pull_request) Successful in 13m6s
Two Steam Deck findings from a field report of "the decoder was not found, it fell back to H.264 — but sometimes HEVC worked". **The 4:4:4 advertisement was a promise nothing checked.** `VIDEO_CAP_444` rode the "Full chroma" setting alone. That was safe while a software HEVC decoder existed underneath it; M8 removed one (there is no permissively licensed HEVC CPU decoder, so `software_decodable_codecs()` is H.264|AV1). The host grants 4:4:4 on HEVC ONLY, and answers the resolved chroma in the Welcome before the client builds a decoder — so on a device with no 4:4:4 decode the toggle did not cost crispness, it cost the entire codec: the Vulkan rung refuses the shape at construction, VAAPI refuses it too, there is no CPU rung, and the session reconnects on H.264. AMD has no HEVC 4:4:4 decode on any silicon, so every Deck with that switch on lost HEVC. It is per-profile and default-off, which is exactly why it looked intermittent — a "Work" profile lost HEVC where "Game" kept it, same box, same host. Gated on `hevc_444_hardware_decodable`, which asks the driver through the SAME code the rung uses at construction (`VkH265Decoder::probe_stream_support`), so the advertisement and the rung that must honour it cannot disagree. Both depths are required, not either: with HDR on the host may resolve 4:4:4 10-bit, and a device offering YUV444_8 but not YUV444_10 would land in the same hole. Answering from the Vulkan rung alone is exact rather than approximate — it is the only rung in this build that implements 4:4:4 at all (`pf_vaadec::profile_for` errors on chroma_format_idc 3, pf-dxvadec refuses anything but 4:2:0, the CPU rung is 8-bit 4:2:0). Deliberately NOT extended to VIDEO_CAP_10BIT/HDR: all three rungs implement 10-bit 4:2:0, so a Vulkan-only probe there would withdraw HDR from boxes whose VAAPI/DXVA rung decodes it perfectly — a real regression against a case never observed. The bit arithmetic moves into `video::video_caps_for` so the part that was wrong is testable without a GPU, a host or a Hello; the test is verified non-vacuous against the planted original defect. **`--probe-decode` described a different device from the one that streams.** The RADV video-decode opt-in sat AFTER the --list-adapters/--probe-decode/--list-audio /--pair early exits, so the triage tool never had it. Measured on a Deck (canary e22af40f), same binary back to back: bare `--probe-decode` printed "vulkan video decode: no", "driver decode ops: none (0x0)", "no queue family advertises VIDEO_DECODE"; with RADV_PERFTEST=video_decode in the environment, "YES" and "H.264, H.265, AV1, VP9". Any Deck triage that consulted it reached the opposite of the truth. Hoisted to the top of `run`, ahead of every early exit — nothing touches Vulkan before it (`main` calls `run` directly). Gates, in the Linux container: fmt, plain `cargo build` (not only --all-targets), `clippy --all-targets -D warnings`, and 185 tests. |
||
|
|
bd86598d97 |
fix(decky): the client update the plugin offers was never once detected
The QAM has offered to update the client since 0.24, and on every Deck it has
answered "up to date" — including right now, with a client a day out of date.
The check asks flatpak for the remote's commit and compares it to the installed
one, and it named the app id with no branch: `flatpak remote-info punktfunk-origin
io.unom.Punktfunk`. The punktfunk remote publishes `stable` AND `canary`, so that
ref is ambiguous and flatpak refuses it — "Multiple branches available" — rather
than picking one. One branch INSTALLED does not help; the ambiguity is on the
remote. The call failed on every box, every time, and the failure returned
`available=False`, which the panel renders as good news. Hence: the plugin
appeared to update only itself.
Every query now names the ref in full, resolved once by `_flatpak_ref()` off the
exported tree (no subprocess — `_client_argv` is on the path of every headless
call). That resolution also carries the SCOPE, so a system-wide install is no
longer invisible to a check that hardcoded `--user`, and the launcher pins the
same `--branch=`, so the client we start is the client we check and update.
A check that cannot run now says so instead of reporting up-to-date: the flatpak
leg reports `client_error` exactly as the native leg already did. Dressing that
failure up as good news is the whole reason this went a week unnoticed.
Also: the button no longer promises "+ client" when the client is manual-only and
the tap can only print a command.
Verified on the Deck (192.168.1.253, canary, user scope) by running both code
paths against the real install, minutes apart:
pre-fix available=False remote=''
post-fix available=True remote=ca010668 (installed e22af40f)
and `flatpak {info,remote-info,update}` all accept the `id//branch` form there.
37 backend checks pass, 6 of them new and about exactly this.
|
||
|
|
c3ecc29117 |
feat(pf-capture): the zero-copy path never asked the compositor for buffer headroom
Wave-2 PW5 stage 2, on the number stage 1 just made visible. `build_dmabuf_buffers` set `SPA_PARAM_BUFFERS_dataType` and stopped there — no `SPA_PARAM_BUFFERS_buffers` at all, so the pool depth the whole zero-copy safety argument rests on was entirely the producer's choice, and we never even expressed a preference. This asks for 8 (min 2, max 16). A **Choice Range**, deliberately, not a fixed count. SPA intersects the consumer's and producer's Buffers params, so a fixed 8 against a producer that can only afford 4 empties the intersection and the link stalls in "negotiating" with no error anywhere — the exact trap that cost this codebase the entire Linux cursor channel once, when a 256^2 cursor-meta max failed to intersect Mutter's fixed 384^2 offer. With a range the producer clamps into it and negotiation still succeeds; the min stays at 2 so nothing that works today stops working. The numbers, and what they are not: 8 buffers is ~133 ms of pool at 60 Hz and ~33 ms at 240 Hz, well past the ~3-4 ms capture-to-fence latency PW3/PW4 measured, with room for a second frame in flight. 16 is a ceiling rather than a request — a 4K 4:4:4 buffer is ~25 MB, so 16 of them is ~400 MB of compositor allocation. These are the values we ASK for; what a producer actually allocates is what stage 1's census line reports, and that line is the one to trust. Scoped to the dmabuf pod only. The mappable and SHM-only builders are untouched: their consumers copy out of the buffer inside `.process`, so pool depth is not part of their correctness argument. A test pins the pod SHAPE — Choice, Range, Int children, values default-first — so a later simplification cannot quietly turn the range back into a number and take the negotiation down with it. Gates green at CI parity; on-glass negotiation on each producer is stage 2's own gate and is reported with the stage-1 census numbers. |
||
|
|
6d550530fe |
feat(pf-capture): nothing had ever counted the compositor's buffer pool — the number every zero-copy safety argument rests on
Wave-2 PW5 stage 1, and the one stage with no risk at all.
The zero-copy capture path dups the dmabuf fd, publishes the frame, and hands the SPA buffer
straight back to the producer at `.process` return — while the encode thread has not yet imported
it, let alone read it. The code says so itself ("content stability across the brief import/encode
window relies on the compositor's buffer-pool depth, like any zero-copy capture"). That depth is
therefore load-bearing: it is the ONLY thing standing between us and the producer overwriting a
buffer mid-read.
And it had never been measured. Not logged, not asserted, not even requested — `build_dmabuf_buffers`
set `SPA_PARAM_BUFFERS_dataType` and nothing else, so whatever the producer picked is what we got,
silently.
This adds the `add_buffer`/`remove_buffer` stream callbacks PipeWire has always offered and logs the
count once per distinct depth: `pool_depth`, `high_water`, and the latest-frame-only `drained`
count beside it. One line per session on a stable pool (`.process` runs at the capture rate — an
unconditional log would be 240 lines a second of the same number), a second line if a
renegotiation changes the depth.
`high_water` is tracked separately from `live` because a renegotiation frees the pool before
re-allocating it: any decision keyed on the live count would read that dip as "the pool shrank".
`remove` saturates at zero rather than wrapping, so an unmatched remove cannot report `u32::MAX`
buffers.
Measurement only — no behaviour change, and no consumer of the number yet. PW5's later stages need
it (a deeper encode pipeline widens the overwrite window by a full frame period), but the number is
worth having regardless of whether those stages ever land: it is the answer to "is our zero-copy
capture actually safe on this compositor", and until now the honest answer was "nobody knows".
3 tests pin the once-per-depth logging, the renegotiation dip, and the saturating remove.
Gates green at CI parity.
|
||
|
|
e22082ac2a |
Merge pull request 'No audio over Bluetooth on iOS: .defaultToSpeaker is an override that outranks A2DP' (#126) from worktree-ios-bluetooth-audio-route into main
ci / bun-nix (push) Successful in 37s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 17s
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 6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 12s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 19s
docker / builders-arm64cross (push) Successful in 14s
ci / web (push) Successful in 2m17s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m32s
ci / docs-site (push) Successful in 2m37s
ci / rust-arm64 (push) Successful in 3m44s
apple / swift (push) Successful in 1m37s
ci / rust (push) Successful in 6m29s
docker / deploy-docs (push) Failing after 6m50s
release / apple (push) Successful in 9m45s
apple / screenshots (push) Successful in 5m52s
Reviewed-on: #126 |
||
|
|
4f5ca5f9bc |
docs(pf-capture): KWin/RADV was the last untested producer — it has no fence either
Closes PW4's one remaining gap. The Steam Deck switched to Desktop Mode gives KWin on RADV, the combination none of the earlier legs covered, and it reports no implicit fence like every other: gamescope + NVIDIA (RTX 5070 Ti) NoFence Mutter + NVIDIA (RTX 5070 Ti) NoFence gamescope + RADV (Deck VANGOGH) 300/300 NoFence, mean 23us, p99 <=100us KWin + RADV (Deck desktop) no fence (older build's wording: waited=false) That is every compositor x vendor this fleet has. PW4 retires with no outstanding doubt rather than "probably fine except one box we never tried". Measured with the Deck's OWN already-authorized binary rather than a scratch build, because KWin grants zkde_screencast_unstable_v1 per EXECUTABLE PATH: it resolves /proc/<pid>/exe against a .desktop's Exec= and caches the grant on first connect, so an unregistered path is refused outright and registering one needs a re-login. The fence probe is pre-existing capture-path code, so a build from July answers the outcome question perfectly well — and nothing of the user's was modified to get it. Comment-only; no behaviour change. fmt + pf-capture clippy -D warnings green. |
||
|
|
07f6d6f324 |
fix(apple): .defaultToSpeaker outranks Bluetooth, so every headset lost the stream
ci / bun-nix (pull_request) Successful in 57s
ci / docs-site (pull_request) Successful in 1m21s
ci / web (pull_request) Successful in 1m40s
ci / rust-arm64 (pull_request) Successful in 2m42s
apple / swift (pull_request) Successful in 1m55s
apple / screenshots (pull_request) Skipped
ci / rust (pull_request) Successful in 12m44s
Field report on 0.25, iOS: "no audio over Bluetooth ... plays through speakers
if Mic input is enabled".
Both halves are one bug. `micEnabled` and `echoCancel` both default to true
(EffectiveSettings.swift), so the DEFAULT iOS session is `.playAndRecord` — and
that branch set `.defaultToSpeaker`. That option is not the polite preference it
reads as: it is an output OVERRIDE, and it outranks an A2DP route. Wired
headphones beat it, Bluetooth does not, so a cable is the one way to test it and
get the right answer — which is what the comment sitting on it asserted
("headphones/BT still win"). Every Bluetooth listener on the default settings got
the phone's own speaker instead. Turning the mic off was the accidental
workaround the reporter found: that path takes `.playback`, which routes to A2DP
happily and always did.
The earpiece problem `.defaultToSpeaker` was reaching for is real —
`.playAndRecord` really does park the built-in output on the receiver. So solve
it against the route we were ACTUALLY given rather than pre-emptively: after
activation, if the current output is `.builtInReceiver`, override to the speaker;
anything external (Bluetooth, wired, CarPlay, AirPlay) is left strictly alone.
That override is a property of the current route — iOS drops it whenever the
route changes, which is exactly what lets a newly-connected headset win — so it
has to be re-applied per route. Hence the route-change observer: without it,
dropping Bluetooth mid-stream would hand the game to the earpiece. Registered
only for a `.playAndRecord` session (a `.playback` one needs no steering),
removed in stop() before the session deactivate, with deinit as a backstop.
Deliberately NOT adding `.allowBluetooth`: it would make a headset's mic usable,
but buys that by dragging the whole route onto HFP/SCO and collapsing game audio
to narrowband. High-quality A2DP output plus the built-in mic is the better trade
for a game-streaming client.
Verified: builds clean on arm64-apple-ios17.0 (the triple that actually compiles
these `#if os(iOS)` blocks — a plain `swift build` is macOS and skips them),
arm64-apple-tvos17.0, and macOS; 257 Swift tests pass, 0 failures.
On-glass iPhone + Bluetooth listen still owed.
|
||
|
|
54666e66da |
Merge pull request 'The Apple TV client had no way to show its statistics overlay' (#125) from worktree-tvos-stats-shortcut into main
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 13s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 15s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 14s
ci / bun-nix (push) Successful in 53s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 14s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
ci / web (push) Successful in 1m17s
ci / docs-site (push) Successful in 1m27s
apple / swift (push) Successful in 1m34s
docker / builders-arm64cross (push) Successful in 15s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 27s
ci / rust-arm64 (push) Successful in 2m24s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m12s
docker / deploy-docs (push) Failing after 1m49s
ci / rust (push) Successful in 5m6s
apple / screenshots (push) Canceled after 0s
release / apple (push) Canceled after 4m52s
Reviewed-on: #125 |
||
|
|
5872dfc649 |
feat(library): a plugin launch kind, so a scanner can publish tiles the host cannot name
apple / swift (pull_request) Successful in 1m40s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m34s
ci / web (pull_request) Successful in 3m46s
ci / bun-nix (pull_request) Successful in 54s
ci / rust-arm64 (pull_request) Successful in 5m54s
android / android (pull_request) Successful in 7m29s
ci / rust (pull_request) Successful in 21m2s
The 2026-08-05 review made `launch.kind = "command"` operator-only, and a reconcile refuses
on the FIRST offending entry — so rom-manager, whose every ROM is `<emulator> <args> <rom>`,
stopped putting anything in the library at all. Playnite hit the same wall and was rescued
with a typed kind the host resolves itself; there is no fixed scheme for "whichever emulator
the operator configured, with the core and flags they chose", so that trick does not
generalise.
So the entry now carries an opaque key and nothing executable, and the host asks the plugin
that owns it what to run — at launch time, over the loopback UI port and per-boot secret it
already registered. A stolen plugin token stops being command execution: planting an entry is
not enough, because the live plugin answers 404 for a key it never published. Nothing
executable is persisted or served to a client, and an emulator that moved is picked up on the
next launch instead of leaving a dead tile (the same reasoning as `xbox` resolving its AUMID
at launch time).
The host still SPAWNS it, because only the host can put the process where the stream can see
it: on Linux the line is either gamescope's own argv or a spawn carrying the session's
compositor env, and the returned child is what session-game-lifetime tracks to know the game
exited. A plugin spawning the emulator itself would land it outside both.
- library/plugin_launch.rs — the ask: blocking ureq, bounded body, absolute cwd, no control
characters, and a log line for every way it can come back empty
- library/launch.rs — `plugin_recipe` tried before both per-OS resolvers, plus
`launch_is_resolvable` so the async handshake probe never makes the blocking call
- native.rs — the session's `resolve_launch` moves onto `spawn_blocking`
- plugin-kit — `serveUi({launch})` serves `POST /__launch`; and `SyncError` finally renders
its cause, which is why a host refusal with a fully explanatory 403 could reach a plugin's
own UI as nothing but "Decode error"
|
||
|
|
bed58b75b6 |
feat(apple): the statistics overlay is reachable on tvOS
ci / web (pull_request) Successful in 1m0s
ci / bun-nix (pull_request) Successful in 1m11s
ci / docs-site (pull_request) Successful in 1m18s
apple / swift (pull_request) Successful in 1m34s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 4m5s
ci / rust (pull_request) Successful in 6m27s
An Apple TV session had no way to the stats overlay at all. Every other client cycles it in-stream — Ctrl+Alt+Shift+S on the desktops, a three-finger tap on touch — and tvOS has neither a keyboard nor a screen to tap, so the only route was Settings before connecting (or a profile). The docs' own "cycle with" table simply had no row for it. Two surfaces, because an Apple TV may have a controller in the room or only the remote: - Select + X on a controller, cycling one tier per completion. Built like Android's mic chord (Select + Y) and deliberately disjoint from the escape chord — X is none of its four buttons, so reaching for one can never trip the other. Read off the wire mask like the escape chord, so a Select the hold-Select gesture has turned into a guide can't cycle the overlay on its way past. Available on every Apple platform: a controller in both hands is exactly the case the keyboard combo and the three-finger tap can't serve. - Hold Play/Pause on the Siri Remote. Its right-click is therefore deferred until the press resolves — a tap still right-clicks, delivered on release with the release trailing by TAP_PRESS — because a right button held for half a second is a context menu on every desktop this streams. A non-forwarding slot now claims the stats chord's elements too, alongside the escape chord's: on tvOS an unclaimed button's press stays the system's and the chord would silently never complete. Tests pin both chords' masks against their GameController alias lists, that the two overlap only on Select, and that the claim list covers both without duplicates — the failure mode is nothing happening, with nothing logged. |
||
|
|
ce31a9ddfd |
Merge pull request 'The Steam Deck updater kept sabotaging its own next update, and hand-deleting a file was the only way through' (#122) from worktree-steamdeck-update-bunnix-dirt into main
ci / docs-site (push) Successful in 1m8s
ci / web (push) Successful in 1m17s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 16s
ci / bun-nix (push) Successful in 20s
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 8s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 5s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
docker / builders-arm64cross (push) Successful in 7s
ci / rust-arm64 (push) Successful in 2m36s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 48s
docker / deploy-docs (push) Successful in 28s
ci / rust (push) Successful in 12m23s
Reviewed-on: #122 |
||
|
|
8fe834c89b |
Merge pull request 'Every gamescope session ended in a SIGSEGV at exit — the Vulkan device was being destroyed after the driver had gone' (#124) from worktree-gamescope-exit-segfault into main
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 13s
ci / bun-nix (push) Successful in 25s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 11s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 40s
ci / web (push) Successful in 1m1s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
ci / docs-site (push) Successful in 1m45s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m0s
docker / builders-arm64cross (push) Successful in 11s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m19s
docker / deploy-docs (push) Successful in 29s
ci / rust (push) Canceled after 3m41s
ci / rust-arm64 (push) Canceled after 3m42s
arch / build-publish (push) Successful in 13m16s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 22m19s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 23m3s
Reviewed-on: #124 |
||
|
|
744bcb468b |
feat(host/wire): a jumbo path can now be PROVEN — and the shipped grow never could
Wave-2 PW7a: a PyroWave session on a proven-jumbo LAN should START at the big shard, because it is the one codec that can never be re-keyed mid-stream (its client parses chunk-aligned AUs in windows of the `Welcome` value, read once over the C ABI). At an 8908-byte shard that is ~6× fewer datagrams per frame — ~49k → ~8k pps at 550 Mb/s — and proportionally less window-tail padding. THE BLOCKER FOUND FIRST: the whole jumbo leg was dead code, not just the missing half. quinn caps a peer's MTU-discovery search at `min(MtuDiscoveryConfig::upper_bound, the OTHER side's advertised max_udp_payload_size)` (`quinn_proto::connection::mtud::SearchState::new`), and `EndpointConfig::max_udp_payload_size` defaults to 1472. Nothing in the repo had ever touched `EndpointConfig`, so raising the host's PROBE ceiling — all `stream_transport_idle` did — could never make discovery settle above 1472, and the shipped mid-session grow's `settled >= sealed_datagram_bytes(target)` gate was unreachable on every path that has ever existed. Two smaller contributors, fixed here too: the watcher stopped sampling the moment `settled >= 1472`, discarding the very climb the proof needs, and a session sealed ABOVE the 1500-byte default was never checked against the path at all. The advertisement is raised on the CLIENT endpoint, under the same `jumbo_wire_mtu()` opt-in as the probe ceiling, because it is not free: quinn sizes its endpoint receive buffer `max_udp_payload_size × max_receive_segments × BATCH_SIZE`, so on a GRO-capable Linux/Android client that is ~2.9 MiB at the default and ~18 MiB at jumbo (47 KiB → 288 KiB on Apple/Windows). Consequence: jumbo now needs the opt-in on BOTH ends. Without it, every byte on the wire and every byte of buffer is exactly what it was. WHY THE GROW IS AS SAFE AS THE CLAMP, which is not obvious — the failure modes are opposite. A stale clamp only makes datagrams smaller than they had to be; a stale grow seals an oversized datagram onto a 1500-byte path, where it is silently dropped, and a PyroWave session cannot recover from that for its whole life. Mirroring the clamp's keying is therefore NOT sufficient. So the memory is demoted: the persisted verdict only decides whether it is worth WAITING for a proof, and what authorises the grow is a LIVE re-proof on the very connection being welcomed — `conn.stats().path.current_mtu` ≥ the sealed target, i.e. a datagram of exactly that size acked by this client, on this connection, seconds ago. The moved laptop cannot inherit anything: its new path's live MTU is 1472 and the grow does not happen, whatever the memory says. The remembered half is keyed strictly anyway — `(local_ip, peer_ip)`, so a verdict earned over the host's 10 GbE NIC does not apply to the same peer over Wi-Fi or a VPN — and carries the operator target it was proven under plus a 6 h TTL. It is erased by any contrary evidence: a lower settle, a session that ended before the window closed (what a client staring at black does), a changed opt-in, or a constrained-path clamp that disagrees. The proof-wait is on the bring-up critical path (`handshake.rs` sends the `Welcome` and only then kicks the display prep), so it is bounded at 300 ms, exits the instant the proof lands, and is entered ONLY for a path a previous session already proved. Its worst case is the moved laptop, and that is self-limiting: that session's watcher erases the verdict. MEASURED, NOT ARGUED: `mtu_discovery_climbs_only_as_high_as_the_peer_advertises` (`#[ignore]`d, loopback — whose own MTU is 64 KiB, so configuration is the only thing that can stop the search), on .21: leg A (server opted in, client NOT): settled at 1472 B UDP payload <- the dead-code proof leg B (both opted in): reached 8972 B in 5 ms <- the fix, and its speed Leg A is the finding restated as an experiment. Leg B says the climb costs ~5 ms once both sides advertise it, so the 300 ms proof-wait is ~60x the loopback convergence time — enough headroom for a real LAN's RTT and per-probe ack delay across the ~11 probes the search takes. Still owed: the A/B on a real jumbo LAN segment (9000-MTU NIC + switch on both ends) — pps per frame, wire/pin ratio, and a PyroWave session observed starting at 8908. Not runnable without the hardware. |
||
|
|
20f4d23f2d |
test(pw6): the streamed-AU trap is real — and at 2 % loss it costs exactly nothing
PW6 shipped behind a knob because one pre-registered risk was unmeasured: a streamed frame whose FINAL block is lost has no totals, so where the whole-AU path hands the consumer a usable blurred partial, a streamed frame may deliver nothing. PyroWave clients opt into partial delivery unconditionally, so this would have been a live behaviour change for every one of them. Measured now, three ways, instead of reasoned about. `tools/loss-harness` gains a partial-delivery leg: FEC pinned OFF, chunk-aligned AUs, deliver_partial ON, realistic 1408/200 geometry, and AU sizes swept across the whole 1..=200-shard range of FINAL-block sizes — because the final block's size is what bounds the exposure. Loss is injected per packet from a seeded xorshift rather than through `loopback_drop_period`, whose deterministic 1-in-N would systematically always-or-never hit the final block, which is the entire question. `tc netem` on `lo` was deliberately not used: the in-process model gives exact per-frame attribution, needs no sudo, cannot disturb a box running a live desktop session, and — decisively — can drop precisely the final block. Leg 1, deterministic (drop exactly the last block, 200 frames): whole-AU delivers 200 partials and 0 losses; streamed delivers 0 partials and 200 total losses. The trap is real and, when it fires, total. Leg 2, random loss, 20 000 frames per cell, same seed and sizes for both shapes. At 2 % the two are indistinguishable — 20000/20000 partials and ZERO vanished frames on both, matching the analytic bound E[loss^k] over final-block sizes k (~1e-4). The gap only appears at 30 % (99.94 % vs 100 % rescue) and 50 % (99.79 %). `complete` is 0 throughout by construction: with FEC off and ~500 packets per AU, essentially every frame is damaged — which is the regime the partial path exists for. The spike gains `--wire-chunk` and a streamed loopback path, so the wire shape is reachable end to end outside a real client: `poll_chunk` drains the AU, `begin_streamed_frame_at`/`seal_streamed_chunk`/`seal_streamed_finish` seal each piece, and the client byte-compares the reassembly. On 120 real PyroWave AUs the streamed legs (56.5 and 2.0 chunks/AU) and the whole-AU control emit a byte-identical 47 373 568-byte stream with 0 mismatches — the cut changes the wire shape and not one byte of content, and with the knob unset it does not engage at all. A new `#[ignore]`d GPU test closes the picture question on real hardware with a BUSY card (gradients + checker + noise), never a flat fill: chunks are whole windows, exactly one `first` and one `last`, the AU decodes through the client's own window walk, and luma PSNR lands at 40.2 dB. Unset the knob and the test refuses to run, which is the default-off claim verified rather than asserted. Verdict recorded in the plan: KEEP IT OFF. The 2 % tie is an argument about typical loss, but the failure is not graceful when it fires and the measured win is host send-side pipelining that nobody has yet put a millisecond number on. |
||
|
|
49f5c815ea |
feat(pf-encode): PyroWave can stream its AU to the wire — and newest-wins was never in the way
PW6 was gated on one question: what happens to the client's newest-wins draining when a PyroWave AU arrives in pieces, given that `Session::set_deliver_frame_parts` refuses to combine with an all-intra stream. The answer is that the doc and the plan conflated two different axes, and the question never applied to this package. Host STREAMED_AU chunks change only the WIRE shape. The reassembler completes such a frame exactly like a whole one (`block_count != 0 && blocks_ok == block_count`) and hands up ONE Frame, so the frame channel still sees one entry per AU and the drain is untouched. What newest-wins genuinely cannot survive is the client's SEPARATE prefix delivery, and the mechanism is sharper than "assumes whole AUs" said: `FrameChannel::pop` counts QUEUE ENTRIES and takes one entry to be one AU. With parts on, one AU pushes several, so `len > 1` stops meaning "the consumer is behind" — the drain fires mid-AU, returns a SUFFIX and clears that same AU's prefixes. For PyroWave that is fatal rather than lossy: the sequence header lives in window 0 of every AU (`au_dims` reads it there), so every frame would arrive headerless, and `FramePart`'s own orphan contract would have a correct consumer abandon essentially all of them. Written into `pop`, `set_deliver_frame_parts` and the handshake, together with what a fix would take (skip whole SUPERSEDED AUs, never split one). That answer shrinks what this package may claim, so the code says so plainly. `encode_frame` is synchronous: the whole AU exists before the first chunk can be polled, so `poll_chunk` is not "emit as produced" and there is no encode/send overlap here (PW6 ⟂ PW5, confirmed). And with the client still receiving one whole Frame there is no decode-while-arriving either — the "~7 ms, decouple e2e latency from AU size" framing needs client work this commit does not do. What IS left is real and host-side: the whole-AU path FEC-protects, packetizes and seals the entire ~830 KB AU before its first datagram may leave the socket, while the streamed path seals and paces each FEC block as it completes. All of the cutting lives in the shared `pyrowave_wire` helper, which compiles and unit-tests on every platform, so both backends' `poll_chunk` / `supports_chunked_poll` are thin delegations — the Windows backend cannot be compiled from a Linux box, and logic written into it directly would ship unverified. Chunks are whole numbers of framing windows because `build_au` gives each window exactly ONE kind; that also makes them shard-aligned for free, which is what the sealer's sentinel bases require. Dense mode never streams (no window framing to cut on). `poll()` now errors while a chunk cursor is live — the trait's one-drain-method-per-AU contract, where double-emitting would put the same bytes on the wire twice under one frame index — and `reset()` drops the cursor so a rebuild cannot splice a dead AU's tail onto a fresh one. No new Encoder trait method, so neither the TrackedEncoder forwarding trap nor the EncoderCaps default trap is in play. Shipped OFF: `PUNKTFUNK_PYROWAVE_STREAMED_AU=1` arms it, `PUNKTFUNK_PYROWAVE_CHUNK_KIB` tunes the 256 KiB target. The pre-registered partial-delivery trap is real and now has a named cost — an unpinned streamed frame (final block lost) is excluded from partial delivery, where the whole-AU path still hands the consumer a usable blur, and PyroWave clients opt into partials unconditionally. The netem loss-harness leg is the prerequisite for default-on and has not been run. |
||
|
|
9e7713eecf |
fix(gamescope): every session ended in a SIGSEGV at exit
Each gamescope-backed session left a coredump behind. It happened after the compositor had finished its work — "Primary child shut down!", then the crash — so the stream itself looked fine and it surfaced only as a steady drip of coredumps and a non-zero exit from the spawn. It is a static-destruction-order bug, not a race and not anything gamescope does wrong at runtime. `g_device` (CVulkanDevice) and `g_output` (VulkanOutput_t) were plain globals, so glibc ran their destructors from `__run_exit_handlers` once main() returned. Those destructors call back into the driver — `~CVulkanCmdBuffer` -> `vk.FreeCommandBuffers`, `~CVulkanTexture` -> `vk.Destroy*` — but the Vulkan ICD has already been torn down and unloaded by then, so each call jumps through a function pointer into an unmapped page. The faulting address equalling the instruction pointer is the signature: #0 0x00007fe8fd1d1070 in ?? () #1 CVulkanCmdBuffer::~CVulkanCmdBuffer at rendervulkan.cpp:1543 #9 std::vector<unique_ptr<CVulkanCmdBuffer>>::~vector (g_device+1792) #10 CVulkanDevice::~CVulkanDevice at rendervulkan.hpp:768 #11 __run_exit_handlers / exit() Patch 0006 gives both globals storage that is constructed exactly as before but never destroyed; a union member is destroyed only if the union's destructor says so, and ours deliberately does not. Nothing needs freeing there — the process is exiting and the kernel reclaims the device, its command buffers and every GPU allocation. Both objects are needed: pinning only the device relocated the fault into ~VulkanOutput_t. The `.pfhdrN` level deliberately stays at 4. It is a capability tier the host probes before it spawns, and this patch adds no capability — bumping it would advertise a tier that does not exist. Per the PKGBUILD's own rule this ships as a `pkgrel` bump instead. Verified on an NVIDIA box, all six patches `git am`-ing onto the pinned upstream commit and then a RELEASE build (the shipped configuration): version banner 3.16.25-7-gea635c1+pfhdr4 (marker intact) patched, real spawn shape (2752x2064@120 --steam --xwayland-count 1) 6/6 exit 0 distro control, same shape SIGSEGV Not filed upstream, though it is not punktfunk-specific and would apply as-is. Unrelated and left alone: `--xwayland-count 0` dies much earlier, in main() at wlserver.cpp:3215, dereferencing a null `gamescope_xwayland_server_t`. Punktfunk always spawns with `--xwayland-count 1`, so that path is never taken here. |
||
|
|
9232631299 |
feat(pf-encode): PyroWave had no encode split — so the one cost this program protects was unmeasurable
Wave-2 PW1's exit criterion, and the instrument it needed. VAAPI and direct NVENC both log a PUNKTFUNK_PERF submit split. PyroWave did not — which meant the single encoder the GPU-priority work exists to defend was the one you could not put a number on. Adds per-frame timing of the synchronous encode (whole `submit`: CSC + encode + fence wait + packetize, which for this backend IS the encode), summarised every 2 s as mean/p50/p99/max. p99 rather than mean-only on purpose. The failure patch 0005 describes is a TAIL event — frames going ~2 ms to 15-18 ms at 95 % game load while the mean barely moves — so a mean-only readout would report "fine" straight through the thing being measured. WHAT IT MEASURED — .21, RTX 5070 Ti (610.57.04), GRID 2 benchmark loop saturating the GPU at 54-87 %, PyroWave 1080p, same binary both arms (only CAP_SYS_NICE differs), 30-frame windows with the warm-up window dropped: arm p50 p99 worst frame default priority (refused) ~2.6 ms ~6.4 ms 9.5 ms REALTIME granted ~3.2 ms ~4.4 ms 5.4 ms REALTIME granted (repeat) ~3.35 ms ~4.8 ms 5.1 ms p99 down ~30 %, worst frame roughly halved, for ~0.6 ms on the median. For a streaming encoder that is the right side of the trade — the tail is what becomes a visible hitch. This CONTRADICTS the patch's only prior datum (RTX 4090 / Windows / WDDM: "did not reduce the spikes"), so patch 0005's header now records the Linux/NVIDIA result beside it, with an explicit "do NOT delete this patch on the strength of the WDDM result — the two stacks disagree". Header prose only; the diff hunks stay byte-identical and `git diff crates/pyrowave-sys/vendor/` is untouched by this commit. Caveats recorded rather than buried: the arms were not interleaved and the game load drifted between them, capture was frame-starved (~2.5 fps) so this is encode latency under contention and not a full-rate stream, and it is two granted runs against one refused run. The direction held across all 25 windows. Also worth knowing for anyone repeating this: `encode_fps` is a VACUOUS metric on this rig. A headless gamescope with no real content emits ~12 fps, so both arms simply report the capture rate. Measure latency, not throughput. Gates green at CI parity. |
||
|
|
0cd946acb5 |
Merge pull request 'The virtual Steam Deck pad's udev rule names a group that four of six install paths never create' (#123) from worktree-steamdeck-group-and-secret-gaps into main
ci / web (push) Successful in 2m26s
ci / docs-site (push) Successful in 1m49s
ci / bun-nix (push) Successful in 42s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 11s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 29s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 14s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 18s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 13s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 19s
ci / rust-arm64 (push) Successful in 4m56s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m32s
ci / rust (push) Successful in 5m51s
docker / builders-arm64cross (push) Successful in 17s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 2m12s
docker / deploy-docs (push) Successful in 6m40s
arch / build-publish (push) Successful in 13m48s
nix / flake (push) Successful in 13m56s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m42s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 28m30s
Reviewed-on: #123 |
||
|
|
62a6fa9fac |
fix(packaging): create the punktfunk group everywhere the udev rule needs it
ci / bun-nix (pull_request) Successful in 29s
ci / docs-site (pull_request) Successful in 1m37s
ci / web (pull_request) Successful in 2m39s
ci / rust-arm64 (pull_request) Successful in 4m10s
ci / rust (pull_request) Successful in 6m50s
nix / flake (pull_request) Failing after 23m28s
60-punktfunk.rules chgrp's the usbip vhci attach/detach nodes to a dedicated
`punktfunk` group (security-review 2026-08-05 M-4: writing `attach` materialises
an arbitrary emulated USB device, so it must not ride on `input`). Four of the
six install paths shipped that rule in 0.25.0 without ever creating the group.
chgrp then failed, the nodes stayed root:root 0644, and the virtual Steam Deck
pad silently never attached — while `usermod -aG punktfunk` failed outright with
"group 'punktfunk' does not exist".
Affected and fixed:
* arch — post_upgrade() called only _ensure_update_group, so every box that
reached 0.25.0 by `pacman -Syu` missed it; post_install was correct.
* nix — no users.groups.punktfunk at all, though host.users' own description
already promised the usbip/vhci pad. Declares it now and adds
host.users to both groups.
* bazzite sysext — a group is host state and cannot ride an image, and the
deb/rpm scriptlets that would create it never run there.
* steamdeck install.sh/update.sh — handled `input` only. Both now create the
group and join it: running that script IS the statement "make my
Deck a host with native pad passthrough".
deb and rpm were correct throughout (one postinst/%post for install + upgrade).
Also on the Deck path: web.env secret hygiene. install.sh's `chmod 600` sat
inside the create-only branch despite a comment calling it "the idempotent belt
for a pre-existing file", and update.sh never touched the config dir at all — so
an install set up once and only updated since kept web.env world-readable
(0644) with the console password and session secret in it. Both scripts now
harden ~/.config/punktfunk to 0700 and web.env to 0600 on every run, and say so
loudly, because a chmod does not un-leak an already-readable secret: the
password still needs rotating.
Both group blocks are `if ensure_group ...` rather than `ensure_group || true`:
a failed groupadd must not fall through to a usermod against a nonexistent
group, which under `set -e` aborted install.sh after the long build and
update.sh before the service restart (verified: exit 6, no restart).
Docs: the group is now documented where people actually look — the per-distro
guides, install.md, steamos-host.md, a new troubleshooting entry for "pad
arrives as an Xbox 360 controller", and the uninstall pages. The 0.25.0 notes
gain the "group does not exist" caveat and turn the password bullet from
"consider rotating" into a real instruction, and CHANGELOG records the known
issue against the breaking change that introduced it.
Verified: bash -n on all four scripts; the arch scriptlet's post_upgrade driven
in a container (creates the group, idempotent on re-run); the ensure_group
helper and both membership branches, including a control that reproduces the
original bug (chgrp to a missing group leaves the node root:root 0644); the
find -perm /0077 probe across 0644/0640/0604/0600/0400 on GNU findutils;
`nix flake check --no-build` (the exact CI gate) and a NixOS eval showing
alice.extraGroups == ["input","punktfunk"]; docs-site build + typecheck.
|
||
|
|
d402e9b996 |
Merge pull request 'A compositor pin silently vetoed dedicated game sessions — and a mid-bring-up mode switch was killing GNOME outright' (#121) from worktree-dedicated-session-pin-and-recovery into main
apple / swift (push) Successful in 1m41s
ci / web (push) Successful in 1m13s
ci / docs-site (push) Successful in 1m53s
ci / bun-nix (push) Successful in 34s
ci / rust (push) Successful in 5m31s
ci / rust-arm64 (push) Successful in 5m16s
android / android (push) Successful in 6m30s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 19s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 7s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 39s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 14s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 16s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 25s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 17s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 41s
deb / build-publish-client-arm64 (push) Successful in 1m31s
docker / builders-arm64cross (push) Successful in 9s
docker / deploy-docs (push) Successful in 47s
apple / screenshots (push) Successful in 6m18s
windows-host / package (push) Successful in 11m16s
windows-host / winget-source (push) Skipped
deb / build-publish (push) Successful in 6m0s
windows-host / canary-manifest (push) Successful in 30s
deb / build-publish-host (push) Successful in 6m46s
arch / build-publish (push) Successful in 12m33s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 14m41s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 14m37s
Reviewed-on: #121 |
||
|
|
f23e0df64c |
fix(host): a compositor pin silently vetoed dedicated game sessions
ci / bun-nix (pull_request) Successful in 26s
ci / web (pull_request) Successful in 1m4s
apple / swift (pull_request) Successful in 1m46s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m59s
ci / rust-arm64 (pull_request) Successful in 2m31s
android / android (pull_request) Successful in 7m23s
ci / rust (pull_request) Successful in 8m5s
`PUNKTFUNK_COMPOSITOR` is documented as "which backend to drive", but it also quietly discarded `game_session=dedicated`: `resolve_compositor` gated the dedicated route on `!overridden` and logged nothing either way. A host whose pin was a forgotten validation leftover therefore went on displaying "dedicated" in the console while every launch landed in the desktop instead — for 30 days on the box that surfaced this, the only evidence being the ABSENCE of a log line. The pin still wins, since it is the operator's explicit hand-set knob, but it now says so and names itself. Two further holes the same triage turned up: - The pin put its backend into `available()` unconditionally AND skipped `apply_session_env`'s `XDG_CURRENT_DESKTOP` scrub, so `pick_compositor` could never return `None` — the one place `try_recover_session()` is called from. A pinned host whose gnome-shell had segfaulted therefore spent every connect on 8 doomed `RemoteDesktop.CreateSession: ServiceUnknown` retries while the operator's configured `PUNKTFUNK_RECOVER_SESSION_CMD` sat unreachable behind that arm. Liveness is now read on both paths, and a pin aimed at a dead session takes the recovery exit with an error naming the pin. `needs_live_session()` exempts gamescope, which stands its own session up — pinning it on a headless box stays supported. - A mode switch accepted before the pipeline existed was served the long way round: build at the now-stale mode, then immediately rebuild at the new one in the stream loop. That burns a display create, capture attach and encoder open on every such connect, and because the rebuild is deliberately create-before-drop it stands up two Mutter `RecordVirtual` monitors ~400 ms apart — which segfaults mutter 50.4 inside `meta_monitor_manager_rebuild` and takes down the whole desktop session, along with the game just launched into it (so the GAME looks like what crashed). Bring-up now adopts the newest queued mode and builds once, carrying over the H2/H3 correction ack that the replaced rebuild would have sent. Verified on a real Linux host (192.168.1.21, x86_64): `cargo clippy --workspace --all-targets --locked -- -D warnings`, `cargo fmt --all --check` and the punktfunk-host + pf-vdisplay test suites all clean. The gate was proved non-vacuous against a planted `compile_error!`. |
||
|
|
12f39e1967 |
fix(steamdeck): the updater stops dirtying the checkout it just pulled into
`update.sh --pull` could abort with "Your local changes to the following files would be overwritten by merge: web/bun.nix" — before a single service was restarted — and the only way past it was to delete the file by hand. The updater did it to itself. web/bun.nix is generated (bun2nix, a pure function of web/bun.lock) but committed, because the Nix build fetches node_modules only from it. The web step ran `bun install --frozen-lockfile` without --ignore-scripts, so web's `postinstall` (`bun2nix -o bun.nix`) rewrote that tracked file on every update. Harmless while the committed file is in sync — but main carried a stale web/bun.nix from |
||
|
|
8387e48ac6 |
docs(pf-capture): the fence wait is already free — PW4 retires into this comment
Wave-2 PW4's outcome. The package proposed moving the producer-fence wait off the PipeWire loop thread, and was pre-registered to be ABANDONED if the wait turned out to already be free. It is, on every producer and vendor measured — including the one where implicit sync actually exists. Steam Deck, RADV VANGOGH, gamescope producer (built in distrobox pf2, run on the host): samples=300 mean_us=23 max_us=48 p50=<=100us p99=<=100us signaled=0 no_fence=300 timed_out=0 failed=0 p99 in the first bucket is the plan's own abandonment condition, and the outcome split explains why: 300 of 300 buffers reported NoFence. Same on both NVIDIA producers (gamescope and Mutter's virtual output — the exact no-explicit-sync case the comment cites as the reason the wait exists). So `wait_read_ready` here is one ioctl and a return, not a block. Moving it to the consumer side would buy nothing measurable and would take on the hazard the package itself names — a slot holding a not-yet-ready dmabuf, and `repeat_last` re-waiting a fence it already consumed. Not a trade worth making for 23 microseconds. The 100 ms budget stays: it guards a producer that DOES fence, which is a real thing even if nothing in this fleet does it. KWin/AMD is the one combination still unmeasured, and the histogram from the previous commit is deliberately kept as the way to re-check — run with PUNKTFUNK_PERF=1 and read the p99 bucket. Comment-only; no behaviour change. Gates green at CI parity. |
||
|
|
fb60bf653e |
feat(pf-capture): instrument the fence wait PW4 wants to move, before moving it
Wave-2 PW4, step one of one-so-far. The package's own first line is "investigation step first
(measure, then decide)", and it is pre-registered to be ABANDONED if the wait's p99 is ~0 — so the
instrument ships before the change, not after.
A per-session histogram of `wait_read_ready`, taken on the PipeWire loop thread, which is exactly
where the wait is expensive: that thread is the compositor's consumer, so time blocked there delays
buffer recycling for the NEXT frame. Logged under PUNKTFUNK_PERF at the same cadence and gate the
encode backends use for their submit splits, so a perf run reads as one instrument: samples, mean,
max, p50/p99 bucket, and the Signaled/NoFence/TimedOut/failed split.
Buckets are coarse on purpose (100us -> 10ms, plus overflow). The decision this feeds is binary —
a p99 in the first bucket means the wait is already free and PW4 becomes a comment correction; a
p99 past 1ms is a real stall against a 16.6ms frame budget. Edges are placed so those two worlds
cannot be confused, and anything past the last edge reports as overflow rather than clamping into
the top bucket ("worse than 10ms" is a distinct finding).
The outcome split sits next to the timings because "the wait is short" and "there is nothing to
wait for" are different results with different consequences, and one data point already shows the
second: on gamescope/NVIDIA the probe reports NoFence, i.e. that producer attaches no implicit
fence at all.
5 tests pin the arithmetic, including that an empty histogram reports "no answer" rather than a
decisive-looking zero — the failure mode that would retire the package on no evidence.
No behaviour change: the wait still happens where it always did. Gates green at CI parity.
|
||
|
|
2a1c968a0e |
Merge pull request 'A gamescope session told every game its display was 60 Hz — and Fedora had no way to install the build that knows better' (#120) from worktree-gamescope-virtual-display into main
apple / swift (push) Successful in 1m37s
ci / web (push) Successful in 1m42s
ci / bun-nix (push) Successful in 29s
ci / docs-site (push) Successful in 1m37s
ci / rust-arm64 (push) Successful in 5m17s
deb / build-publish-client-arm64 (push) Successful in 1m48s
apple / screenshots (push) Successful in 6m27s
android / android (push) Successful in 9m56s
windows-host / package (push) Successful in 10m52s
windows-host / winget-source (push) Skipped
deb / build-publish (push) Successful in 11m20s
arch / build-publish (push) Successful in 16m0s
windows-host / canary-manifest (push) Failing after 32s
deb / build-publish-host (push) Successful in 12m7s
ci / rust (push) Successful in 17m46s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 12s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 10s
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 8s
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 8s
docker / builders-arm64cross (push) Successful in 23s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m15s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 2m8s
docker / deploy-docs (push) Successful in 33s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 13m51s
nix / flake (push) Failing after 14m39s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m35s
Reviewed-on: #120 |
||
|
|
b815e00a87 |
fix(pf-zerocopy): one dmabuf timeout condemned every later capture on the host, forever
Wave-2 PW3.
The raw-dmabuf passthrough has two very different reasons to switch itself off, and they shared one
`AtomicBool`:
* the encoder repeatedly failed to import what this compositor allocates — unrecoverable, a
driver fact, and the reason this latch was written (it stops the encode-stall recovery
rebuilding the same doomed encoder five times and then ending the session, on every connection,
forever);
* the dmabuf-only capture offer never negotiated — which can simply mean the compositor was
mid-restart.
Sharing the flag made the second as permanent as the first. One timeout, and EVERY later session on
the host captured CPU frames until the process was restarted — including sessions against a
completely different compositor and a different node, which had never failed at anything. Nothing
said so; the arm line PW2 added would have shown `cpu` with no explanation.
Now the two causes have the lifetimes they should have, in a `RawDmabufLatch` that owns both:
* Import failures stay sticky. Unchanged threshold (3 consecutive), unchanged hazard coverage.
* Negotiation timeouts get a retry budget of 2 — one retry, deliberately small: each failure
costs a ~10 s stall, so a larger budget is paid by the user in dead air. One retry survives the
mid-restart transient; a compositor that genuinely never accepts keeps the same identity, so it
latches on the second try, one extra stall per host lifetime versus the old behaviour.
* A capture that negotiates credits the budget back, so an evening of reconnects against a
compositor that failed once cannot accumulate its way into a latch.
* BOTH are keyed to a capture identity (node id + portal bit). A new node — fresh virtual output,
compositor restart, the Bazzite Gaming↔Desktop switch — is a genuinely different question and
earns a fresh dmabuf attempt instead of inheriting a verdict about something else. The SAME
capture keeps its verdict, which is what preserves the 10 s-stall protection the latch exists
for.
The session-open line now carries the latch state, so `cpu` is no longer ambiguous between "this
host was never going to do dmabuf" and "something failed earlier and we are still living with the
verdict" — only the second is a bug worth chasing, and only the second is now visible as one.
Atomics rather than a lock because `note_import_ok` is on the per-frame import path; everything
else runs at pipeline build or on failure. The state machine is tested against a local instance
rather than the process-wide static — seven tests covering both lifetimes, the identity clear, the
same-identity hold, the budget credit, and the cause naming.
One honest note on the identity: it is the PipeWire node id, not the "(compositor-id, modifier
list)" pair the design sketched. Node id is what capture actually has at that point, and it changes
on exactly the events that matter here (new virtual output, compositor restart, session switch).
Keying on the modifier list too would need the list before the importer is built, which is the
wrong order.
|
||
|
|
eb8c943572 |
feat(packaging): ship punktfunk-gamescope on RPM and apt too
ci / web (pull_request) Successful in 1m4s
ci / bun-nix (pull_request) Successful in 26s
apple / swift (pull_request) Successful in 1m40s
apple / screenshots (pull_request) Skipped
ci / rust (pull_request) Failing after 2m3s
ci / docs-site (pull_request) Successful in 1m41s
ci / rust-arm64 (pull_request) Successful in 2m44s
android / android (pull_request) Successful in 4m50s
nix / flake (pull_request) Failing after 16m12s
Until now the patched gamescope reached exactly four kinds of box: the Bazzite/Fedora-Atomic sysext, the Arch package, the SteamOS installer and a NixOS option. Everyone else was told to build gamescope from source. A traditional Fedora-family box — Nobara, plain Fedora, the HTPCs people actually stream from — therefore ran stock gamescope by default, which streams SDR, cursorless, and tells every game its display is 60 Hz. That is not a user error; there was no package to install. Both new packages REPACK the binary CI already builds rather than building gamescope again: it is a ~10-minute meson compile of an unrelated tree, cached per distro base because the binary is soname-coupled to it. The Arch PKGBUILD stays the one recipe that builds from source, because that is what makepkg is for. - packaging/gamescope/punktfunk-gamescope.spec + build-gamescope-rpm.sh. Version is derived from the binary's own banner (3.16.25.pfhdr4) — the only source that cannot drift from what is in the package. rpmbuild's automatic ELF Requires are what stop an f43 build installing on f44. - packaging/debian/build-gamescope-deb.sh, same shape, with dpkg-shlibdeps for Depends. - rpm.yml packages and publishes it beside the host RPMs; deb.yml gains a cached gamescope build (keyed on packaging/gamescope/** alone) and packages it into the existing publish loop. Both legs are best-effort, matching the sysext's existing rule: no binary, no package, and the host stays on its current SDR path. Neither package Provides or Conflicts with gamescope — it installs as /usr/bin/punktfunk-gamescope and only the sessions the host starts itself resolve it, so a box's own Game Mode keeps using the distro binary. Both refuse to package a binary without the +pfhdr marker. That marker is the host's entire capability probe, so a build that lost the patches would install fine and then silently stream SDR with no cursor. Verified: build-gamescope-deb.sh produces an installable .deb from a stand-in binary (correct version derived from the banner, 0755 tree, control fields) and exits 1 on an unmarked one. The .spec is not yet exercised — no rpm tooling on the box I had; CI's Fedora leg is its first run. |
||
|
|
102f550bba |
feat(host): use the new gamescope capabilities, and say so when the mode is lost
Pass --custom-refresh-rates (patch level 3+) and --pipewire-composite-external-overlay (level 4+) on both spawn paths, with the same probe-then-pass shape the HDR and cursor flags already use. A stock gamescope has neither flag and gets neither, which is exactly today's behaviour. New knob PUNKTFUNK_GAMESCOPE_REFRESH_RATES=60,90,120 widens the set a session offers in Steam's in-session display settings. The rate the session actually runs at is always included, so it can only add options; junk entries are skipped rather than failing the host, because the worst a typo can cost is the extra option the operator wanted. And the part that would have turned a week of field triage into one log line: warn_if_mode_lost(). --nested-refresh is the ONLY refresh a headless gamescope has, and it reaches a gamescope-session-plus solely through the GAMESCOPE_BIN wrapper, which the session script is free to lose — a sessions.d file sourced with `set -a` can reassign GAMESCOPE_BIN, and one that sets GAMESCOPECMD outright skips the whole builder. When that happens the stream still runs, still looks right, and the client's own fps counter still reads the negotiated rate (the encode loop repeats the held frame), while the game underneath is capped to 60. Nothing anywhere said so. It warns rather than refusing, deliberately: verify_managed_spawn_flags refuses because its retry resolves a different plan, but a relaunch here would hand the session the same environment and lose the mode the same way, so refusing would only loop. Fails open on the same rule as the flag check — nothing to compare against says nothing. Also corrects the comment above the launch env, which claimed CUSTOM_REFRESH_RATES "generates the mode the session ADVERTISES … what makes games see the real refresh". It never did: no upstream gamescope has --custom-refresh-rates, so gamescope_has_option gated it off and the variable was inert. That belief is why the real lever went unexamined. configuration.md gains the new knob and a warning on PUNKTFUNK_MAX_FPS, which also lowers the refresh the session REPORTS on gamescope — the docs said it does not cap the stream, which is true of the wire and not of what games are told. Linux-verified on Ubuntu: cargo check --all-targets, clippy -D warnings, 133 tests (2 new), cargo fmt --check. |
||
|
|
818531a26e |
feat(gamescope): a headless session now reports its own mode, and the perf overlay reaches the stream
Two new patches on the pinned upstream, and the marker patch moves last so the banner is stamped after the capabilities it advertises. 0003 — headless: advertise the virtual display's mode and refresh rates. A headless gamescope is how we give a game a display: we pass the client's exact mode and the session runs at it. It never told anyone. CHeadlessConnector returned empty spans from GetModes() and GetValidDynamicRefreshRates() and reported GAMESCOPE_SCREEN_TYPE_INTERNAL, so update_mode_atoms DELETED the mode-list atom (no resolution list) and wlserver fell through to a one-entry refresh list built from g_nOutputRefresh (no refresh list). With --nested-refresh absent that entry is Init()'s 60 Hz default — which is why a field report on a 1920x1080@120 client saw "gamescope only shows 60hz, and there's no other option", and why Overwatch capped itself to 60 while the stream ran at 120. Populate both from the resolved mode, report EXTERNAL, and add --custom-refresh-rates so the offered set can be widened. gamescope-session-plus has probed for that flag for years; upstream never had it, so the CUSTOM_REFRESH_RATES env it plumbs was a no-op everywhere. 0004 — pipewire: optionally composite the external overlay into the capture stream. That layer is mangoapp: the fps/frametime readout the Deck UI turns on. paint_pipewire has never referenced it on any version, so a consumer whose only view of the session is the node sees the overlay it just enabled not appear, with nothing to configure. Behind --pipewire-composite-external-overlay, off by default, same argument as the cursor flag. Its commit id joins the repaint test — the numbers change while the picture behind them is static, exactly the case the existing test skips. Verified: the series git-am's cleanly onto the pinned 8c676c39, and both new functions were extracted verbatim and compiled with -Wall -Wextra under C++23 against stubs, with unit assertions for the parser and the mode/rate publication (sorting, dedup, the running rate always present, re-entrancy, zero rejected). A full gamescope build was not run — no box here has its dependency set; CI's per-Fedora-major leg is the first real compile. |
||
|
|
608baf63be |
Merge pull request 'Post-sleep sessions still failed on 0.25.0 — the host was holding open the very device its recovery asks PnP to cycle' (#119) from worktree-vdisplay-reap-pnputil into main
apple / swift (push) Successful in 1m40s
ci / web (push) Successful in 1m21s
ci / rust-arm64 (push) Successful in 2m46s
ci / docs-site (push) Successful in 1m20s
ci / bun-nix (push) Successful in 26s
android / android (push) Successful in 6m37s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 12s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 7s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 7s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 12s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
apple / screenshots (push) Successful in 6m8s
deb / build-publish-host (push) Successful in 4m27s
deb / build-publish-client-arm64 (push) Successful in 2m0s
deb / build-publish (push) Successful in 5m36s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m1s
docker / builders-arm64cross (push) Successful in 8s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m23s
ci / rust (push) Successful in 9m44s
docker / deploy-docs (push) Successful in 36s
arch / build-publish (push) Successful in 12m2s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 4m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 4m14s
windows-host / package (push) Canceled after 11m59s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
Reviewed-on: #119 |
||
|
|
767e67caf4 |
feat(packaging): grant the host CAP_SYS_NICE, without which the GPU-priority lever does nothing
Wave-2 PW1, second half. The companion commit wires `PYROWAVE_QUEUE_PRIORITY` into the Linux PyroWave device; this is what makes it work on a packaged host. Measured on .21 (RTX 5070 Ti, NVIDIA 610.43.02), same binary in both arms: as packaged (no capability) every class refused, REALTIME *and* HIGH -> default priority same binary, cap_sys_nice+ep granted REALTIME on the FIRST attempt, no downgrade RADV behaves the same way. So this is not the RADV-specific "expect one downgrade to HIGH" the plan predicted — without the capability there is no elevated priority at all, on any vendor, and the knob is decoration. Worth being precise about what is being granted, because it is a network-facing daemon. CAP_SYS_NICE permits raising scheduling priority (nice, ioprio, affinity, RT class) and nothing else: no filesystem access, no network privilege, no user switching, and it is NOT setuid. The repo already ships exactly this capability on its gamescope binary for the same reason. Two side effects that will otherwise confuse someone debugging: a capability-carrying binary is AT_SECURE, so the loader ignores LD_LIBRARY_PATH/LD_PRELOAD for it (note this box was propped up by exactly such a shim during the ffmpeg-9 soname break — that workaround would now be silently ignored), and core dumps are suppressed by default. Per packaging path, because none of them are the same: - Arch: a `_grant_sched_capability` in the scriptlet, called from post_install AND post_upgrade — a replaced binary is a new inode, so the capability does not survive an upgrade by itself. - Debian: the same setcap in the postinst `configure` branch. - RPM: `%caps(cap_sys_nice=ep)` on the binary in `%files`, which is the rpm-native form — rpm then applies it on install, restores it on upgrade, and verifies it. A `%post setcap` does none of those. - NixOS: `security.wrappers`, because a store path is read-only and shared and cannot be setcap'd. The unit's ExecStart moves to `config.security.wrapperDir` — without that the wrapper exists and the service still runs the uncapped store path, which is the whole failure this fixes. - Steam Deck: setcap in the installer's sudo block. That box needs it most (one small Van Gogh GPU shared between the game and the encode). The binary lives under $HOME, so unlike the /etc drop-ins it survives a SteamOS A/B update on its own and needs no atomic-keep entry — but it does need re-applying after each rebuild, which re-running the installer does. - Bazzite sysext: at IMAGE BUILD time, before mksquashfs. It cannot be done in the merge hook (a merged sysext's /usr is read-only squashfs) and it cannot ride in from the RPM either — rpm keeps capabilities in its own header and `rpm2cpio | cpio` carries only the payload, so the staged file arrives with none. mksquashfs does record security.capability (only security.selinux is excluded), so a setcap on the staging tree is what lands in the image. Needs root/CAP_SETFCAP; a plain-user CI build warns and ships without it rather than failing a release over a performance lever. Every one of them is best-effort and cannot fail an install: a box without libcap, or a filesystem that cannot store capabilities, simply runs at default priority exactly as it does today. Documented in the same PR — the configuration row now says the packages grant it, and running-as-a-service gets a section explaining what it is, how to check it (`getcap`), and how to remove it (`setcap -r`, or just `PYROWAVE_QUEUE_PRIORITY=off`), including the two debugging side effects. Verified: the Arch scriptlet grants the capability from a fake package root exactly as pacman would invoke it, and the resulting binary reaches REALTIME end to end on the RTX 5070 Ti; the RPM spec's %caps line parses under rpmspec in a Fedora 41 container; the NixOS module parses under nix-instantiate; all five edited shell scripts pass `bash -n`. No Rust file changed in this commit, so the CI-parity Rust gates from the companion commit still stand. |
||
|
|
8f9c72877e |
Merge pull request 'An SDK fix could never reach an installed plugin — the runner now carries it' (#117) from worktree-runner-sdk-reconcile into main
ci / bun-nix (push) Successful in 24s
ci / web (push) Successful in 1m9s
ci / docs-site (push) Successful in 1m16s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 15s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 12s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 12s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 18s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 13s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 41s
ci / rust-arm64 (push) Successful in 2m28s
deb / build-publish-client-arm64 (push) Successful in 1m35s
deb / build-publish-host (push) Successful in 4m13s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 2m59s
sdk-publish / publish (push) Successful in 1m52s
docker / builders-arm64cross (push) Successful in 15s
deb / build-publish (push) Successful in 7m16s
ci / rust (push) Successful in 6m52s
arch / build-publish (push) Successful in 7m29s
docker / deploy-docs (push) Failing after 3m54s
windows-host / package (push) Successful in 15m55s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 25s
nix / flake (push) Successful in 14m35s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m52s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 21m11s
Reviewed-on: #117sdk-v0.1.4 |
||
|
|
8f32976349 |
Merge pull request 'Library source settings never opened — the drawer still called the old plugin origin' (#118) from worktree-library-settings-origin-split into main
ci / bun-nix (push) Successful in 31s
arch / build-publish (push) Canceled after 1m8s
ci / rust (push) Canceled after 52s
ci / docs-site (push) Canceled after 1m12s
ci / rust-arm64 (push) Canceled after 1m15s
ci / web (push) Canceled after 1m15s
deb / build-publish (push) Canceled after 55s
deb / build-publish-host (push) Canceled after 45s
deb / build-publish-client-arm64 (push) Canceled after 8s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 14s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Canceled after 19s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 4s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 4s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 17s
docker / builders-arm64cross (push) Canceled after 0s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 13s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 26s
windows-host / package (push) Canceled after 1m54s
windows-host / canary-manifest (push) Canceled after 0s
windows-host / winget-source (push) Canceled after 0s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 53s
Reviewed-on: #118 |