a17aa61c5a6a5e624e0cec504d1a815e15edfc34
1808
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a17aa61c5a |
fix(session/gamelease): four things the first pass got wrong
Found reviewing the riskiest paths of the previous commit. - An install dir of `/games/x` matched a process running out of `/games/xyz`. The image-path check was already component-wise and fine; the command-line check compared raw bytes, so it needed the separator. Two library folders where one name prefixes the other is not a contrived case. - Ending a nested game force-releases kept displays, which is not per-display — so with another client streaming it could retire a display that was never ours. Skip it while anyone else has a live session: the failure mode becomes a game that keeps running, which is the default everywhere else anyway. - A lease nothing polls (no detect signals, or a platform without a matcher yet) sat at "launching" forever in the console. Report it as running: the host did just launch it, and with no watcher it is claiming nothing about the exit. - A game ended after its session was already gone reported no `game.exited` at all — its watcher had stopped with the session, so nothing was left to emit it. That is every grace expiry and every `POST /game/end`, i.e. exactly the ones an operator most wants to see. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
64c0ff96bc |
feat(session): bind a session's life to its game's, in both directions (Linux)
Two of the most-asked-for behaviors, and until now the host could only manage a
sliver of one of them: end the streaming session when the launched game exits,
and — when the operator asks — end the game when the session ends.
Ending the session on game exit already worked, but only for a Steam title under
a bare-spawn gamescope. Everywhere else the host spawned a launch and forgot it:
the pid was logged and dropped, so on KWin, Mutter, wlroots or gamescope-attach a
finished game left the client staring at a desktop. Ending a game was not
possible at all — there was no primitive for it.
The missing piece was never plumbing, it was knowledge: a launch says what to
run, not what the game looks like once a launcher has handed off. So each store
now also reports how to recognize its game (DetectSpec, previous commit's
subject matter, folded in here), procscan turns that into live pids on Linux, and
gamelease turns pids into a lifetime — with four kinds covering how the game got
started:
nested gamescope owns it; its display teardown ends the game
child the host spawned it, in its own process group
matched a launcher owns it; recognized by its store's signals
untracked nothing identifies it — both behaviors stay off, and the host
says so once rather than guessing
A child that exits successfully within 5s was a launcher handing off, not the
game: the lease re-resolves to matched (or to untracked) instead of reporting an
exit. That single rule is what keeps steam://, epic:// and playnite:// launches
from ending a session the moment they start.
Ending a game is destructive, so three rules bound it. It is opt-in
(game_on_session_end defaults to keep). A drop is not a decision — `always`
waits out a reconnect window (5 min) that a returning client cancels by
reclaiming its own game, matched on fingerprint and title. And only ever this
session's game: a pid is adopted only if it started after the launch, so a copy
the player already had open is never touched, and every pid is re-verified
against its start time immediately before being signalled, so a recycled pid
never is. Termination asks first (SIGTERM to the group, 10s) and only then
insists.
Also here, because they are the same decision seen from other angles:
- A management stop is now a deliberate stop. `DELETE /session` sets quit before
stop, so it behaves like a client pressing Stop — the display skips its
keep-alive linger instead of lingering for a session nobody is coming back to.
- `/status` reports what is running (games[]), including a game whose session has
gone and which is waiting out its window, so the console can show it and offer
`POST /game/end`.
- New game.running / game.exited events, filterable like every other kind, which
is the whole polling loop of the community plugin this replaces.
- session_status::register takes a named struct: eleven positional arguments,
half of them same-typed Arc<Atomic…>, is a transposition waiting to happen.
Gates on Linux (.21): check, clippy --all-targets, fmt, and 291 tests green.
Windows (Job Objects, Toolhelp matching) and the GameStream plane are phases 2
and 3; both are inert here, as is macOS, which has no launch path at all.
Design + plan: punktfunk-planning/design/session-game-lifetime{,-implementation-plan}.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
0d9d78398c |
fix(drivers/windows): name each virtual pad for what it is — one shared description read as "the setting did nothing"
deb / build-publish (push) Successful in 9m31s
windows-drivers / probe-and-proto (push) Successful in 42s
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 1m9s
windows-drivers / driver-build (push) Successful in 2m1s
ci / bench (push) Successful in 7m35s
decky / build-publish (push) Successful in 20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
deb / build-publish-host (push) Successful in 10m4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
android / android (push) Successful in 12m30s
ci / rust-arm64 (push) Successful in 13m55s
apple / swift (push) Successful in 5m28s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m41s
deb / build-publish-client-arm64 (push) Successful in 11m7s
windows-host / package (push) Successful in 16m55s
arch / build-publish (push) Successful in 20m3s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8m46s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9m7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10m54s
docker / deploy-docs (push) Successful in 34s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m4s
flatpak / build-publish (push) Failing after 8m23s
docker / build-push-arm64cross (push) Successful in 3m59s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m28s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m48s
ci / rust (push) Successful in 32m19s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m55s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m9s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m53s
release / apple (push) Successful in 29m51s
apple / screenshots (push) Successful in 24m26s
`pf_dualsense.inx` gave all four hardware ids a single %DeviceDesc%, so Device Manager labelled an emulated DualShock 4, DualSense Edge and Steam Deck pad "punktfunk Virtual DualSense". The HID layer was always per-type — device_type picks the PID (09CC for DS4), the report descriptor and the product string — but the one place a user goes to check said DualSense for every choice, which reads exactly like the controller-type setting being ignored. Split into four model lines over the same install section, one description each. No binding, service or descriptor change; stampinf's 9.9.MMdd.HHmm DriverVer increments on every build, so pnputil takes the update. InfVerif on the WDK runner: INF is VALID. Also correct the Slot.pref comment from the previous commit: emulating a DualShock 4 gives up adaptive triggers by construction. HidOutput::Trigger is emitted only by dualsense_proto, and a DS4 has no trigger-effect reports — the host never generates any to send. Rumble and the lightbar remain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6edd700910 |
fix(client/gamepad): honor an explicit controller type — the per-pad arrival was re-declaring the physical pad
The "Controller type" setting reached the Hello and stopped there. Every pad then opened with a GamepadArrival carrying its DETECTED kind, and the host builds each virtual device from that arrival — the session default is only the fallback for a pad that never declares one. So "emulate my DualSense as a DualShock 4" put a DualSense on the host the instant the controller connected, and no amount of reconnecting helped. Apple and the native clients both; Android already applied the setting per pad. The setting now rides the declaration too: explicit wins for every slot, Automatic keeps per-pad detection so a mixed session stays honest. The physical kind still drives the LOCAL feedback paths — a DualSense emulated as a DualShock 4 keeps its lightbar and adaptive triggers, and a Deck keeps its rumble keep-alive. Regressed in |
||
|
|
8362d57001 |
fix(vdisplay/windows): exclusive topology gets re-asserted — a verified isolate is not durable
ci / docs-site (push) Successful in 1m8s
ci / web (push) Successful in 1m40s
apple / swift (push) Successful in 5m9s
ci / bench (push) Successful in 6m36s
ci / rust-arm64 (push) Successful in 10m12s
deb / build-publish (push) Successful in 9m32s
decky / build-publish (push) Successful in 34s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 30s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 59s
android / android (push) Canceled after 13m40s
apple / screenshots (push) Canceled after 8m25s
arch / build-publish (push) Canceled after 13m44s
ci / rust (push) Canceled after 13m45s
deb / build-publish-host (push) Canceled after 12m5s
deb / build-publish-client-arm64 (push) Canceled after 7m5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 2s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Canceled after 0s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 2m28s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 1m57s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 2s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 1s
windows-host / package (push) Canceled after 13m45s
The CCD isolate verifies "sole active desktop" at commit time and is never checked again. On a hybrid Intel+NVIDIA box the internal panel is re-activated moments later (the isolated topology is deliberately not saved to the CCD database so teardown can restore the user's layout, and the post-isolate resize/HDR churn — or the panel's own driver, or display-poller software — re-resolves back to the stored layout). Proven on-glass: seconds after the verify, CCD showed the panel ACTIVE beside the virtual display while the host still believed it was exclusive — cursor and windows can land off-stream, and the lock screen can land on the physical panel. A group-scoped watchdog now re-queries every PUNKTFUNK_EXCLUSIVE_REASSERT_MS (default 2 s, 0 disables) while an EXCLUSIVE isolate is live and re-issues the isolate when a non-managed display crept back, logging who-is-fighting escalation (3×WARN → 1×ERROR → DEBUG). Gated on a new GroupState.ccd_exclusive flag so a Primary group's deliberately-lit panels are never "fixed". Cycles take the state lock via try_lock with a sliced sleep, so teardown's stop+join under the state lock cannot deadlock and is bounded by ~250 ms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6c2c8b6eab |
feat(mgmt/web): surface the host.env encoder pin so a conflicting GPU choice isn't invisible
listGpus gains encoder_pin (PUNKTFUNK_ENCODER when it actually pins something — unset/auto stay null), and the console's GPU card shows it: a muted note when the pin is compatible, an amber warning naming the pinned vendor and the next session's GPU when it contradicts the selection (the host overrides such a pin at session open — without this field the selection just looked broken). Docs updated to say the adapter wins. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
751d1de506 |
fix(encode/windows): a stale PUNKTFUNK_ENCODER pin no longer wedges a conflicting GPU selection
On a hybrid box, picking a GPU in the web console whose vendor contradicts a host.env PUNKTFUNK_ENCODER pin produced an unrecoverable session: the pin won backend selection while the adapter followed the console, the wrong-vendor encoder failed deterministically at submit, the reset ladder burned its 5 in-place rebuilds on it, and the client reconnected into the identical wall forever (~10 s per cycle, no visible reason). Three legs: - windows_resolved_backend() now reconciles: a hardware pin whose vendor contradicts the selected GPU is overridden by the adapter-derived backend (capture + encode share one adapter, so honoring the pin can only fail); open_video warns loudly when a pin loses. The reconciliation is a pure, unit-tested table (resolve_windows_backend). - the QSV wrong-adapter bind is typed TerminalEncoderError, and the stream loop's reset ladder ends the session immediately on it instead of feeding a deterministic config error 5 futile rebuilds. - the un-pinned path was already correct (verified on-glass: NVIDIA preference + no pin = stable AV1 10-bit HDR on NVENC), so the override simply takes the conflict case onto that path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
36e566052f |
fix(apple/cursor): a pointer whose bitmap has not landed must not vanish
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 1m5s
ci / bench (push) Successful in 7m30s
apple / swift (push) Successful in 6m42s
ci / rust-arm64 (push) Successful in 10m2s
decky / build-publish (push) Successful in 27s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 15s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
deb / build-publish-host (push) Successful in 10m7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 14s
android / android (push) Successful in 12m14s
deb / build-publish (push) Successful in 12m47s
windows-host / package (push) Successful in 16m3s
docker / build-push-arm64cross (push) Successful in 24s
docker / deploy-docs (push) Successful in 50s
arch / build-publish (push) Successful in 18m4s
deb / build-publish-client-arm64 (push) Successful in 10m45s
flatpak / build-publish (push) Successful in 7m12s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m25s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m33s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m31s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m7s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m17s
ci / rust (push) Successful in 28m53s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m45s
release / apple (push) Successful in 33m5s
apple / screenshots (push) Successful in 24m18s
`resetCursorRects` wore the host shape only when `hostCursors[st.serial]` hit,
and fell through to `invisibleCursor` on a miss. But a miss is the ROUTINE case,
not a degenerate one, and it is not a reason to hide the pointer:
* State (0xD0) is a per-frame datagram and announces the new serial the moment
the host QUEUES the bitmap on the reliable control stream, so on every single
shape change the client knows a serial before it holds the pixels.
* The shape ring drops the NEWEST under burst (CURSOR_SHAPE_QUEUE = 8) and the
host never re-sends it — it only sends on a serial CHANGE — so a dropped
serial stays un-backed until the pointer next changes shape. Crossing a
toolbar flips arrow/I-beam/hand/resize several times a second, and each flip
mints a fresh serial and a fresh bitmap, so bursts are ordinary.
Either way the pointer BLINKED OUT rather than lagging, and in the dropped case
it stayed gone for as long as the pointer held that shape — reported on glass as
"the I-beam never appears over text fields, every other cursor is fine".
Hold the last worn shape across the gap: only `st.visible == false` (the host
says the pointer is hidden) may hide it now, never a missing bitmap. Worst case
is a briefly stale pointer. This also makes two comments that already claimed
this behaviour true — the shape-rejected warning's "keeping the previous cursor"
and CURSOR_SHAPE_QUEUE's healing claim, both of which were fiction.
Host side is exonerated: on .221 the poller sees the I-beam handle (0x10005,
CURSOR_SHOWING set), and the monochrome AND-over-XOR conversion renders a correct
glyph — black beam, white outline — so the bitmap that goes on the wire is good.
swift build PunktfunkKit + cargo check punktfunk-core green; on-glass owed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
6a9b3b0f28 |
fix(windows/cursor): a re-rendered pointer keeps its handle — re-probe the extent
ci / web (push) Successful in 56s
ci / docs-site (push) Successful in 1m9s
ci / bench (push) Successful in 7m23s
apple / swift (push) Successful in 6m15s
deb / build-publish (push) Successful in 9m17s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
deb / build-publish-host (push) Successful in 9m54s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 42s
android / android (push) Successful in 12m39s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m8s
ci / rust-arm64 (push) Successful in 14m7s
deb / build-publish-client-arm64 (push) Successful in 10m39s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6m38s
arch / build-publish (push) Successful in 19m9s
windows-host / package (push) Successful in 19m10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9m11s
docker / deploy-docs (push) Successful in 31s
docker / build-push-arm64cross (push) Successful in 8m32s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m15s
ci / rust (push) Successful in 31m34s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m38s
apple / screenshots (push) Successful in 25m36s
The GDI shape poller rasterises only when the HCURSOR VALUE changes. But Windows rebuilds the system cursors at a new size whenever the display scale under the pointer changes, and it does that behind a handle that never moves — the shared arrow is 0x10003 for the whole session. So the cache latched whatever size the pointer happened to have when the poller started and never let go. That window is not rare, it is the norm: a fresh virtual display is created at Windows' RECOMMENDED scale and only picks up the client's saved PerMonitorSettings override a beat later, while the poller starts within a second of the monitor appearing. Sample inside it and the session forwarded — and composited — a 96 px pointer over a 100 % desktop for its entire life, which reads on the client as a pointer 3x too large while every other thing on the streamed desktop is correctly sized. Scaling on the client cannot undo it: the bitmap is proportional to the video, it is just proportional to the WRONG scale. Re-read the bitmap's extent on a slow cadence whenever the handle is unchanged — dimensions only, no pixel copy, 4 Hz — and drop the cache when it moved, so the next tick re-rasterises and publishes a new serial. Observing the extent itself rather than a DPI proxy also covers the accessibility pointer-size slider and any other cause of a same-handle re-render. Windows-side clippy -D warnings green via scripts/wincheck.sh; on-glass owed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1421e235f2 |
fix(host): hdr-p010-selftest silently ignored its size argument
apple / swift (push) Successful in 5m41s
ci / web (push) Successful in 1m24s
ci / docs-site (push) Successful in 2m15s
windows-host / package (push) Successful in 11m2s
arch / build-publish (push) Successful in 12m41s
ci / rust-arm64 (push) Successful in 9m56s
ci / bench (push) Successful in 7m52s
android / android (push) Successful in 17m58s
decky / build-publish (push) Successful in 28s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
deb / build-publish (push) Successful in 9m2s
docker / build-push-arm64cross (push) Successful in 9s
docker / deploy-docs (push) Successful in 11s
ci / rust (push) Successful in 21m47s
deb / build-publish-host (push) Successful in 9m27s
deb / build-publish-client-arm64 (push) Successful in 8m34s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m52s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m31s
apple / screenshots (push) Successful in 29m0s
`args` starts AT the subcommand (main builds it with `env::args().skip(1)`), so a subcommand's own optional arguments begin at index 1 — cf. `args.get(1)` in the `service` arm. This arm read `skip(2)`, swallowing the first optional argument. The documented invocation `hdr-p010-selftest 1920x1080 nvidia` therefore picked up the vendor (it is in the second slot) and ran at the 64x64 DEFAULT. A size-only `hdr-p010-selftest 1920x1080` parsed nothing whatsoever and still printed PASS. Nothing warned, because an unrecognised token is the only thing that errors and no token was ever inspected. The size is the entire point of the flag: the arm's own comment says heights like 1080 are not 16-aligned and exercise a different driver path. So the self-test has only ever validated the one geometry that exercises the least, and the sweep's W7 gate — 1920x1080 and 5120x2880 on the RTX box — could not actually run as written. Found by running that gate: both sizes printed "HDR P010 self-test (64x64 ...)" with byte-identical plane layouts (row_pitch=128, expected_total=12288 — those are 64x64's). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
28bbaa5250 |
tools: check and lint Windows-only Rust from a Linux dev box (scripts/wincheck.sh)
ci / web (push) Successful in 1m30s
ci / docs-site (push) Successful in 1m31s
ci / rust-arm64 (push) Successful in 9m50s
ci / bench (push) Successful in 7m15s
apple / swift (push) Successful in 5m41s
android / android (push) Successful in 12m23s
arch / build-publish (push) Successful in 17m26s
decky / build-publish (push) Successful in 23s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 13s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 12s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
windows-host / package (push) Successful in 18m55s
deb / build-publish (push) Successful in 9m9s
deb / build-publish-client-arm64 (push) Successful in 7m29s
ci / rust (push) Successful in 22m14s
deb / build-publish-host (push) Successful in 11m27s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 22m9s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 22m15s
docker / build-push-arm64cross (push) Successful in 9s
docker / deploy-docs (push) Successful in 23s
apple / screenshots (push) Successful in 26m13s
Linux CI never compiles `#[cfg(target_os = "windows")]` code, so a Windows-side edit was
unverifiable until the Windows CI job ran or someone tested on a real box. The pf-capture sweep's
Phases 3–6 used an ad-hoc version of this harness in a scratch directory; committing it means the
next Windows change does not have to rediscover the recipe.
The obvious in-tree command fails, and not because of the Windows code:
cargo check --target x86_64-pc-windows-msvc -p pf-capture
dies in build scripts that compile C for the target — audiopus_sys (cmake wants a VS generator),
then ring (needs an MSVC C compiler plus the Windows SDK headers). Both enter through
punktfunk-core's `quic` feature.
So the script generates a workspace whose members are Cargo.toml copies with `src` SYMLINKED at the
real crate directories — nothing to keep in sync, no copies to go stale — plus a ~30-line stub
punktfunk-core carrying only `Mode` and `quic::HdrMeta`, which is provably all the Windows capture
stack uses. rustls, ring and opus never enter the graph. Every path dep that is not a generated
member points at the real crate.
Covers pf-frame, pf-win-display and pf-capture with --all-targets, so the Windows `#[cfg(test)]`
modules are type-checked too. ~10 s cold, ~1 s warm, into target/wincheck (gitignored).
Verified non-vacuous: a deliberate type error planted in a windows-only file
(idd_push/stall.rs) is caught and fails the run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
0d69ebf60b |
ci(windows): run pf-capture's tests instead of only type-checking them
Phase 0 of the sweep added `clippy -p pf-capture --all-targets` to this workflow, which type-checks all 19 Windows `#[test]`s and executes none of them. They were decorative. The workflow's own comment explains why pf-encode's tests cannot run here — nvenc link-imports NvEncodeAPICreateInstance, which resolves only against the driver's import lib. That reasoning does not extend to pf-capture: it has no encoder dependency at all (`cargo tree -p pf-capture` lists no nvidia/ffmpeg/libvpl/pyrowave), so its test binary links against nothing the runner lacks. Confirmed empirically rather than reasoned: `cargo test --release -p pf-capture` was run on a Windows dev box against a workspace checkout — it linked, executed and passed, building in ~51 s off an existing release target dir. --release keeps it in the one C:\t\release tree, so it does not trip the C1069 disk exhaustion a second debug dep tree causes. 18 of the 19 run here (StallWatch + ring-generation masking, the cursor shape→wire truth table, and `f32_to_f16`'s rounding-carry/saturation edges); all are pure — no Win32, no device, no desktop. The 19th, `hdr_p010_selftest_intel_1080_live`, is `#[ignore]`d because it needs a real Intel adapter. This is Windows-only code no other job can execute, so it was the cheapest coverage still on the table. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1423b63333 |
fix(gamestream): the HDR SDR-downgrade latch gated the capture offer but not the negotiation (X6)
`pf_capture::hdr_capture_failed()` had exactly one consumer: `open_portal_monitor`, which uses it to drop the HDR offer (`want_hdr && !hdr_capture_failed()`). The RTSP negotiation consulted only `gnome_hdr_monitor_active()`. So once the latch was set — an HDR negotiation timed out, the monitor left HDR mode between probe and negotiation, NVIDIA EGL not listing LINEAR for XR30, a pre-50 Mutter — the host kept telling the client HDR while capturing and encoding SDR: `cfg.hdr` flowed to `open_portal_monitor`, which then silently opened the SDR offer. The client renders an SDR stream as PQ. Washed out, wrong gamut, and no error anywhere. The latch is sticky until host restart, so every reconnect repeated it. Consulted at RTSP honor time rather than folded into `host_hdr_capable()` for the same reason as the colour-mode probe next to it: that fn is the STATIC serverinfo capability (it decides whether to advertise SCM_HEVC_MAIN10 at all), and this is a live per-session fact. The native plane needs no equivalent — `capturer_supports_hdr()` is hard-false on Linux, and the latch is a fact about the portal capturer, which that plane never uses. Noted in handshake.rs so it isn't re-derived. Listed in the sweep's confirmed-defect register as cross-cutting/medium, then absent from every phase of its implementation plan. Verification is on-glass (force the latch, reconnect a client that requests HDR, confirm the Welcome/SDP reports SDR). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
59e33ad11d |
Merge origin/main into the pf-capture sweep
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 1m10s
ci / bench (push) Successful in 5m43s
windows-host / package (push) Successful in 10m48s
ci / rust-arm64 (push) Successful in 13m38s
decky / build-publish (push) Successful in 34s
deb / build-publish (push) Successful in 12m32s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 19s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 40s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 15s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 21s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 15s
android / android (push) Successful in 17m7s
deb / build-publish-host (push) Successful in 9m49s
arch / build-publish (push) Successful in 17m59s
ci / rust (push) Successful in 21m32s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6m51s
docker / build-push-arm64cross (push) Successful in 9s
docker / deploy-docs (push) Successful in 26s
deb / build-publish-client-arm64 (push) Successful in 9m55s
apple / swift (push) Successful in 6m10s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 23m10s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 23m24s
apple / screenshots (push) Successful in 24m29s
Upstream landed `fc335b39` (negotiate the cursor around what the encoder can blend) on the same files this sweep restructured. Merged rather than rebased: the sweep MOVED ~2,000 lines out of `linux/mod.rs` into `pipewire.rs`/`portal.rs`/`pw_*.rs`, so a rebase would have re-fought the same conflict in up to nine commits; merging resolves it once with both sides in view. The repo already carries merge commits (`land/sweep-all`). Two conflicts, both resolved by keeping BOTH changes: * `linux/mod.rs` — `PortalCapturer::open` gains upstream's `want_metadata_cursor` AND keeps the sweep's `PortalSession` teardown, so the portal threads now take `(setup_tx, quit_rx, want_metadata_cursor)`. The second conflict was upstream editing inside the region Phase 5.1/5.3 moved out; the split wins and upstream's change is ported to where the code now lives: `choose_cursor_mode`'s new `want_metadata` ladder (4 arms — prefer Embedded when the encoder can't blend, and warn-then-take Metadata when Embedded isn't advertised) is now in `portal.rs`, taken verbatim. * `gamestream/stream.rs` — the pooled-capturer slot keeps upstream's third reuse key (`metadata_cursor`, beside HDR-ness) AND the sweep's `result.is_ok() && capturer.is_alive()` re-pool gate. Both guard the same slot against different failures: theirs against reusing a session negotiated for the wrong cursor mode, the sweep's (L2) against reusing a dead one. Everything else auto-merged, including the `want_metadata_cursor` thread through `host/capture.rs`'s facade and `native/stream.rs`'s `session_plan::cursor_blend_for`. Verified after the merge: workspace `cargo test` green, `clippy --workspace --all-targets` clean on Linux AND on x86_64-pc-windows-msvc, `cargo fmt --check --all` clean, pf-capture 38/38. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
14914bc72f |
test(pf-capture): the suite the splits enable — 38 Linux + 19 Windows tests (Phase 6)
The plan's priority order was "every producer- or OS-controlled parser, blend, geometry guard or negotiation decision", none of which had a test before this sweep (X3). Phase 5's splits are what made most of them reachable without a compositor or a driver. **6.1 `bitmap_extent`** — extracted from `update_cursor_meta`, which is the guard whose own SAFETY proof says a missing bound "SIGSEGVs inside the PipeWire `.process` callback (a segfault `catch_unwind` cannot catch)". Every input except the region size is producer-written. 5 tests: a bitmap that fits (with and without header/pixel offsets); the last-row rule (`stride·(bh−1) + row`, so a bitmap ending flush against the region is accepted rather than rejected by padding that is never read — and one byte short is rejected); anything past the region; degenerate geometry; and four distinct overflow vectors including the near-`i32::MAX` stride the SAFETY comment calls out. One assertion I first wrote was wrong, not the code — with a single row `stride·0 == 0`, so even a `usize::MAX`-wide row is arithmetically in range; the test now exercises the ≥2-row case that really overflows and says why the other is fine (the caller has already capped `bw` at 1024). **6.2 the composite blits** — 8 tests over `composite_cursor` / `composite_cursor_rgb10`: every packed `PixelFormat` arm lands the colour in its OWN channels (so a byte-order slip cannot pass); clipping off all four edges plus six fully-outside positions; zero-alpha, `visible: false` and no-bitmap-yet all drawing nothing; the integer alpha blend; NV12/Yuv444 declined rather than mis-blitted; and for the 10-bit path a bit-exact round trip of an untouched pixel (including the two alpha bits the repack must preserve), the R-at-bit-20 vs R-at-bit-0 distinction between `X2Rgb10` and `X2Bgr10`, and the same clipping. Plus `decode_bitmap_pixel`'s four byte orders. **6.4 the pod builders** — 4 tests. The `dataType` bitmask is pinned per Buffers pod, because each bit is load-bearing in a different direction: the mappable pod MUST include DmaBuf (or gamescope's modifier-bearing format pod wins and the buffer intersection is empty — a link silently stuck in "negotiating"), the SHM-only pod MUST exclude it (or Mutter hands dmabufs and the race-free download path is not), and the dmabuf pod MUST exclude the mappable types (or an HDR session can be handed a MemFd buffer, which Mutter paints 8-bit ARGB32 regardless of the negotiated 10-bit format). Also: every pod parses back through `Pod::from_bytes`; the HDR pods carry MANDATORY PQ + BT.2020 + LINEAR-modifier; and only the NV12 offer pins the colour matrix/range. The `dataType` reader parses the SPA property layout literally (`key, flags, size, type, value`) — a "find the first plausible-looking int" heuristic read the `size` word, which is also 4, and reported the wrong mask. **6.5 `FrameToken` generation masking (W14)** — 2 tests, with `IDD_GENERATION` parked two below the 24-bit boundary so the WRAP is what gets exercised: every minted generation is non-zero, fits the token's field, and survives the pack/unpack round trip `try_consume` performs; and the cleared- `latest` 0 sentinel never matches a live generation. **6.6 the cursor conversion (Windows)** — `convert`'s pixel logic extracted from its GDI plumbing into `mono_planes_to_rgba` / `apply_and_mask_alpha` / `alpha_is_empty`, which is what makes it testable at all (the caller needs a live `HCURSOR` and a screen DC). 6 tests: the four-state AND/XOR truth table in one row; transparency surviving without an invert neighbour; the invert outline covering all eight neighbours, overwriting only TRANSPARENT ones, and clipping at the edges; the alpha-less colour cursor taking alpha from the AND mask; and a short mask not panicking. **6.3 / 6.7** landed with the fixes they guard (`negotiation_plan` in 2.3, `f32_to_f16` in 3.5). 38 Linux tests, up from 6 at the start of the sweep — ci.yml already runs them. 19 `#[test]`s on the Windows side; Phase 0.1's `--all-targets` lint type-checks them all, but note it does not RUN them (the workflow lints only), so the Windows ones execute on a Windows box. clippy --all-targets clean on both targets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
306f4a514d |
refactor(pf-capture): structural splits + collapse the restated signal set (Phase 5)
Refactors LAST, after every defect fix, so no behaviour change hides in a move diff — and so the newly-exposed review surface was already clean when it landed (the WP7 discipline). 28f8fc71's recorded trap (an inline `use super::X` inside a moved body silently changing meaning) was audited first: every `use super::` in the moved code is either at module level — where `super` still means `linux` — or inside a `mod tests` that moved with its parent. 5.1 `mod pipewire` → `linux/pipewire.rs`. It was 1,900 of that file's lines. `mod.rs` is a directory module, so a plain `mod pipewire;` resolves with no `#[path]`. 5.2 out of that, `linux/pw_pods.rs` (the 7 param-pod builders + `serialize_pod` + the PQ constant and its test) and `linux/pw_cursor.rs` (`CursorState`, `decode_bitmap_pixel`, `update_cursor_meta`, `dst_offsets`, both `composite_cursor*`). The pods are the crate's WIRE surface — a missing property is not a compile error but a link that stalls in `negotiating`; the cursor half is producer-driven and bounds-critical. Both are now pure enough to unit-test without a compositor, which is what Phase 6 needs. 5.3 `linux/portal.rs`: the ScreenCast/RemoteDesktop handshake, the cursor-mode choice and GNOME's BT.2100 probe — the async/tokio/zbus control plane, separated from the realtime half. Zero re-exports changed: `mod.rs` re-exports `gnome_hdr_monitor_active` at its old path. 5.4 `idd_push/open.rs` (the whole one-time construction path + `SharedObjectSa` + `AttachTexFail`) and `idd_push/compose_kick.rs`. Read `open.rs` when a session will not START and the parent when one stops flowing; the steady state stays with the parent by decision. Also moved the ~65-line stall REPORTING block out of `try_consume` (the hot loop) into `StallWatch::report`, taking its two correlation counters with it — they were capturer fields nothing else touched. 5.5 `windows/dxgi/selftest.rs`: `p010_reference`, both self-tests, `hdr_p010_convert_bars_on_luid`, `f32_to_f16` and the two test modules — the validation path, none of which runs in a session. The two `pub` entry points are re-exported, so `main.rs`'s subcommand and pf-encode's live e2e keep their paths. (An explicit `#[path]`, like `idd_push`'s children: this file is itself reached through a `#[path]`, so a bare `mod` would resolve to `windows/selftest.rs`.) 5.6 `CaptureSignals`. The seven shared flags/slots were created in `spawn_pipewire`, `_cb`-cloned one by one, passed as seven of `pipewire_thread`'s parameters, and then re-declared as fields on `PwHandles`, `PortalCapturer` AND `UserData` — the same list written four times, each with its own drifting copy of the doc comment. One `#[derive(Clone)]` struct instead (a refcount bump per field), which also makes it structurally obvious that producer and consumer share ONE set. And `CaptureOpts` for the four trailing `bool`s: four adjacent same-typed positional arguments are a silent-transposition footgun — swap `want_444` and `want_hdr` and it compiles, negotiates the wrong pod family, and surfaces as a black screen ten seconds later. 5.7 trait shape, targeted: `set_active(&self)` → `&mut self` (it took `&self` only because the flag happened to be an `Arc<AtomicBool>` — an implementation detail leaking into the contract); `capture_target_id` ⇒ `resize_output`'s pairing rule stated on both methods and in a cluster note; one-line cluster headers over the 13 methods. NOT done from 5.7: taking the gamescope targets as an `open_*` option instead of the trait method. It would put a Linux-only parameter on the cross-platform `open_virtual_output` and on the host's `capture_virtual_output` facade — a `#[cfg]`-shaped wart in two shared signatures to delete one already-`cfg`'d defaulted method whose lifecycle rule 2.5c had already made harmless. Wrong trade under 5.7's own "no churn for no defect fixed" principle. File sizes: `linux/mod.rs` 2,778 → 770 (target ≤900 ✓), `windows/dxgi.rs` 1,374 → 954, and `windows/idd_push.rs` 2,694 → 1,922. The last two miss the plan's ≤850/≤1,300 targets, which were computed against the ORIGINAL line counts — Phases 1–4 added several hundred lines of SAFETY proofs and defect rationale to exactly these files before the split. The moves themselves are the ones the plan specifies; closing the rest would mean inventing new splits it does not call for. Zero behaviour delta. pf-capture 20/20; workspace clippy --all-targets clean on Linux and on windows-msvc. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
530909a154 |
perf(pf-capture): take three per-frame costs off the Windows HDR hot path (Phase 4)
**4.1 (W10) — the HDR convert allocated two views and mapped a constant buffer per frame, inside
the ring slot's keyed-mutex hold.** So the driver's publisher sat blocked on that slot while the
host created `CreateRenderTargetView`s and Mapped/Unmapped a 16-byte buffer whose contents never
change. Both are lifetime-of-mode facts, not per-frame ones:
- the two P010 plane RTVs now belong to the out-ring SLOT, built once in `ensure_out_ring`
(`out_ring` becomes `Vec<OutSlot>`). A driver that rejects planar RTVs — the one hard
requirement of this path — therefore fails at ring build, where such a failure belongs, instead
of on the first frame.
- `inv_src` becomes an IMMUTABLE constant buffer filled in `HdrP010Converter::new(device, w, h)`.
The converter is already dropped by `recreate_ring` on every mode change, so it cannot go
stale. This also deletes the `Map`'s silently-ignored failure: it could leave the chroma pass
sampling at a garbage texel size with no error anywhere.
**4.2 (W15) — `sdr_white_level_scale` ran inside the keyed-mutex hold too.** It is a CCD query that
contends the display-config lock — precisely the contention `DescriptorPoller`'s doc says must stay
off the frame path — and `prepare_blend_scratch` called it while holding the slot. Moved to
`refresh_sdr_white_scale`, called at open and after each ring recreate (where the scratch is
invalidated anyway). "Only on scratch rebuilds" is not the same as harmless when the thing being
blocked is the producer.
**4.3 (W11) — `VideoProcessorSetStreamAutoProcessingMode(vp, 0, false)`.** The comment claimed "no
interpolation/auto-processing" while only setting the frame format; auto-processing's documented
default is ENABLED, so vendor denoise/edge-enhancement was free to run inside every SDR
`VideoProcessorBlt` — altering the pixels we encode and spending video-engine time on the
desktop-capture hot path. Now actually disabled, with each call's purpose stated separately.
**4.4 (the Linux CPU-path map cache) is deliberately NOT done.** The plan gates it on measurement
("measure first, and only if the CPU path still matters") and this box cannot measure a live capture
session; it also needs a frame-buffer return path through `FramePayload::Cpu`, which is a larger
change than the rest of this phase. Left for Phase 7's measured pass.
Compile-verified for windows-msvc locally; pf-capture 20/20 on Linux; workspace clippy
--all-targets clean on both targets. The GPU-capture check and the measured `cap_us` delta are owed
to Phase 7.3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
bc45287dc6 |
fix(pf-capture): Windows IDD-push defects — HDR pin, cursor, recreate, handles, f16 (Phase 3)
Compile-verified for x86_64-pc-windows-msvc locally (a scratch workspace symlinking the real
sources against a stub punktfunk-core — the `quic` feature's ring/opus C builds are what blocks an
in-tree cross-check). Behaviour is owed the on-glass validation in sweep Phase 7.3.
**3.1 (W1/F3) — the HDR pin asserted a flip it never verified.** `poll_display_hdr` discarded
`set_advanced_color`'s `bool` and then wrote `now.hdr = self.client_10bit` — the DESIRED state in
place of the observed one. On a display that cannot be flipped (the state this file already logs as
"Downgrade point D" at open) that broke in both directions: wanting HDR, the fabricated `true`
differed from `current`, so two poller samples drove `recreate_ring(true, …)` and rebuilt the ring
FP16 while the driver composed 8-bit BGRA — every publish dropped by the driver's format guard,
`recovering_since` expiring, `try_consume` bailing: a permanent 3-second reconnect loop. Wanting
SDR, the fabricated `false` MATCHED `current`, so no recreate ever fired and the ring stayed BGRA
against an FP16 composition — the same dropped-publish outcome, silently. Now it re-reads
`advanced_color_enabled` and follows what the display actually composes, with a one-shot error
naming want/observed/returned. A not-yet-settled read costs one debounce cycle, never a wrong ring,
which is why this does not block the frame path on a settle poll the way `open_on` does. Downgrade
point D's error now carries the same pair, so `Some(false)` (display says no) and `None` (the CCD
read failed) are distinguishable.
**3.2 (W2, W3, W6) — cursor correctness.**
- W2: the poller's desktop rect was captured once at open and used forever, for BOTH the
desktop→frame offset and the `in_rect` visibility test — while both mid-session mode-change
paths (`resize_output`, `poll_display_hdr` → `recreate_ring`) keep the same poller. After an
in-place resize the pointer was clipped to the old rect and offset by a stale origin. It is now
a SEED: the poll thread re-queries on its existing 250 ms reattach cadence, keeping the last
good value on `None` (a transient CCD failure must not park the rect at zero and report every
position invisible), which keeps the CCD call off the encode thread as `DescriptorPoller`
demands.
- W3: `composite_forced` tested `cursor_sender.is_none()`, but §8.6's rationale is "no cursor
CHANNEL" — and the delivery just above it is explicitly allowed to fail non-fatally, which is
precisely the state needing the rescue. It was the one state that skipped it: a negotiated
channel that failed to create or deliver left a cursor-excluded target with NO pointer at all.
Now `cursor_shared.is_none()`, evaluated after that binding.
- W6: `cursor()` degraded poller→shm correctly, but the BLEND path — the only consumer that
matters in the composite model, since the Windows encode loop never attaches `frame.cursor` —
read the poller directly with no `alive()` check and no fallback, so the documented fallback and
the spawn-failure warning were both untrue for exactly those sessions (a dead poller meant
pointer-less frames, not a degraded pointer). One `live_cursor()` now serves all three
consumers and LATCHES the source, because the two keep independent serial namespaces and
interleaving them poisons the client's shape cache.
**3.3 (W4, W5, W14) — recreate hardening.**
- W4: `recreate_ring` committed `display_hdr`/`width`/`height` BEFORE the fallible
`create_ring_slots` (VRAM pressure at a large new mode — exactly when resizes happen), leaving
a failed recreate emitting frames stamped with the new geometry against the old ring, the old
generation and an unchanged header. Slots are built first; nothing after the commit point fails.
- W5: a recreate never cleared the driver's status words, and `wait_for_attach` — the only
classifier of TEX_FAIL/BIND_FAIL and the only source of the LUID rebind — runs at open ONLY. A
stale `OPENED` therefore made a failed re-attach look healthy while the recover-or-drop bail
reported nothing. Now cleared before the Release generation store (plus `status_logged`), and
the 3 s bail prints the live `(driver_status, detail, render_luid)` the way `next_frame`'s 20 s
bail already did. The four-field read is one `driver_diag()` helper instead of four copies of
the same unsafe block.
- W14: `IDD_GENERATION` is a full `u32` but the publish token carries 24 bits and `unpack` masks
what it reads, so past 2²⁴ recreates `tok.generation != self.generation` would be permanently
true — every frame rejected. Masked at the single mint point, and 0 skipped (it is also the
cleared-`latest` sentinel).
**3.4 (W8, W9, W12) — handle hygiene.** `shared_object_sa`'s security descriptor is a `LocalAlloc`
nobody freed: leaked twice per open and once per ring recreate. It is now an RAII `SharedObjectSa`
whose `Drop` `LocalFree`s it and whose `as_ptr()` only lends a borrow — which also makes the
"descriptor must outlive the attributes" rule structural instead of a comment. The PyroWave fence's
shared NT handle, created per capturer and never closed, becomes an `OwnedHandle` (the encoder holds
its own duplicate, so closing ours is safe). `cursor_blend`'s `cbuf_scale` is cached only on a
successful `Map` — caching unconditionally wedged the HDR/SDR cursor scale for the session after one
transient failure.
**3.5 (W7) — `f32_to_f16` swallowed the rounding carry.** `sign | half_exp | (half_mant + round)`
ORs a mantissa carry into bit 10, so for every ODD biased exponent (bit 10 already set) the carry
vanished and the result came back ~2× low: `1.9998779 → 1.0`, `0.49996948 → 0.25`. Only values one
ULP below a power of two are affected — precisely what a gradient test pattern is full of — so this
made `hdr-p010-selftest` FAIL a correct shader. Composed additively, with 4 tests (18 asserted bit
patterns, a round-trip property over the self-test's scRGB values, saturation) — all verified
numerically against a standalone reference on this box, including a scan confirming old-vs-new
diverges ONLY on the carry cases. Phase 0.1's `--all-targets` lint is what lets these compile in CI
at all.
pf-capture 20/20 on Linux; workspace clippy --all-targets clean on Linux and windows-msvc.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
9a7b3a46d0 |
fix(pf-capture): gamescope cursor — auth without setenv, frame-space coords, live targets (2.5)
**2.5a (L4) — stop `set_var`ing `XAUTHORITY` from a live multithreaded host.** The old connect swapped the process-global var around each `RustConnection::connect` under a mutex. That lock serialises this source against itself and nothing else: `getenv` takes no lock, so every OTHER thread's read raced it — and the concurrency is by construction, since `attach_gamescope_cursor` runs while the PipeWire thread is starting up (libspa plugin load, EGL/CUDA init). The primary path now parses the MIT-MAGIC-COOKIE-1 entry out of the given file itself and hands it to `DefaultStream::connect` + `RustConnection::connect_to_stream_with_auth_info` — the same two steps `RustConnection::connect` performs internally, minus its env-derived auth lookup — so nothing in this process touches the environment. The env swap survives only as a fallback for a file we cannot parse, and a wrong cookie pick cannot do damage: the server rejects it and the fallback (which does libxcb's full family/address match) takes over. Sharing `pf_vdisplay`'s process-wide env lock was not the fix — wrong layer, and it would still not fix `getenv`. **2.5b (L6) — publish the pointer in FRAME coordinates.** `QueryPointer` answers in the nested root's space, but `CursorOverlay::x/y`'s contract is frame pixels, and gamescope's `-w/-h` (nested root) and `-W/-H` (output + PipeWire node) are independent knobs — at `-W 1280 -H 720 -w 640 -h 360` they differ by 2×, so the pointer drew at a fraction of its real position. The root size comes free off the setup reply we already parse; the negotiated frame size arrives from the PipeWire thread's `param_changed` through a new `Arc<AtomicU64>` (`0` = not negotiated ⇒ pass through, as before). Scaling is computed in `i64` — a 5K coordinate times a 5K width overflows `i32`. Position only: the bitmap stays at root scale, warned once so a mismatched session is visible. **2.5c (L7) — the targets are a PROVIDER, not a snapshot.** The list was discovered once, before the game launched. gamescope creates the game's Xwayland at launch and advertises only the FIRST in any child's environ (verified on this box: `--xwayland-count 2` makes `:2` and `:3`, only `:2` is advertised), so the game's display was invisible — and when the connected Big Picture display then reported "gamescope is not drawing the pointer here", the source blanked the cursor for the whole game session, which is the exact regression the module doc says it fixed. The worker now re-runs the provider every 2 s: it adopts new Xwaylands, reconnects dead ones, and `spawn` no longer returns `None` on an empty list — a stream that starts before the game converges instead of staying cursorless. The provider is a host-facade closure, same one-way-edge shape as `FrameChannelSender`. **2.5d (L8) — bound the teardown join.** `Drop` joined the worker unbounded while the worker blocks in `RustConnection` replies with NO read timeout, so a peer that stops answering but keeps its socket open hung capturer teardown — on the session path. Now: a completion channel, `recv_timeout(250 ms)`, then detach with a warning (the thread only touches its own X connections and an `Arc`'d slot). 7 new tests, all host-runnable: the cookie reader against a hostile `.Xauthority` (wrong protocol, wrong display, wildcard entry, truncation mid-length-prefix, an over-long length prefix, a missing file), display-string parsing, and the root→frame mapping incl. the 5K overflow. pf-capture 20/20; workspace clippy --all-targets clean on Linux and windows-msvc. Phase 7.2 owes the on-glass nested-gamescope validation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3f4ad08869 |
test(encode/ffmpeg_win): extract the decision logic and pin it with unit tests
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
decky / build-publish (push) Successful in 20s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
deb / build-publish (push) Successful in 9m9s
docker / build-push-arm64cross (push) Successful in 10s
docker / deploy-docs (push) Successful in 24s
deb / build-publish-host (push) Successful in 10m47s
android / android (push) Successful in 14m53s
deb / build-publish-client-arm64 (push) Successful in 10m38s
windows-host / package (push) Successful in 17m40s
ci / web (push) Successful in 1m46s
ci / docs-site (push) Successful in 1m50s
arch / build-publish (push) Successful in 11m37s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 21m11s
apple / swift (push) Successful in 6m4s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 21m15s
ci / bench (push) Successful in 5m47s
ci / rust-arm64 (push) Successful in 9m11s
ci / rust (push) Successful in 21m50s
apple / screenshots (push) Successful in 25m14s
The QSV open-failure fallback (1,400 lines, 23 unsafe, 0 tests) follows the vaapi.rs treatment: the device-free decisions now live in named functions with their contracts pinned — the per-vendor zero-copy default matrix (AMF on-glass-validated on, QSV opt-in), the PUNKTFUNK_FFWIN_POLL_MS clamp-before-µs-conversion (the 27.7-hour-spin class), the readback routing table with its mid-stream depth-change guard, the swscale source map, the QSV display-remoting latency contract (async_depth=1/low_power=1/ look_ahead=0/forced_idr=1/scenario) and the AMF no-B-frames contract, and the per-vendor zero-copy pool bind flags. A probe smoke rides along #[ignore]d for the runner. No FFI plumbing chased; no behavior changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fc335b39e9 |
fix(host/encode): negotiate the cursor around what the encoder can blend
EncoderCaps::blends_cursor's contract said the HOST must fall back to capturer-side compositing when a cursor-as-metadata session lands on an encoder that can't composite — but that host half was never built: open_video warned and the session streamed WITHOUT a pointer (confirmed on the VAAPI dmabuf and libav-NVENC CUDA paths; latent on vulkan RGB-direct/native-NV12). The negotiation is now caps-aware, ahead of capture, on both planes: * pf-encode grows cursor_blend_capable() — the pre-open dispatch mirror (sibling of linux_native_nv12_ok) answering whether the resolved backend composites frame.cursor; its pure core is test-pinned arm by arm. * Native plane: handshake::cursor_forward grants the cursor channel only where the resolved backend can blend (the capture-mouse flip makes the host draw the pointer on demand); denied sessions keep the pre-channel path — the compositor EMBEDS the pointer, never cursorless, never doubled. The Welcome's HOST_CAP_CURSOR bit is computed once and read back at both session-wiring sites instead of recomputed. SessionPlan::output_format additionally keeps every cursor-blend session off producer-native NV12 (the arm with no CSC to fold a cursor into), and vulkan RGB-direct now yields to a cursor-blend session even when pinned (EFC cannot composite; the open logs the override). Windows plans cursor_blend=false via the new shared cursor_blend_for() rule — the IDD capturer composites the pointer itself, and asking the encoder anyway fired the blends-cursor warn spuriously on every cursor-channel session. * GameStream plane: the hardcoded cursor_blend=true is gone. The portal source asks for cursor-as-metadata only when the resolved backend blends, otherwise negotiates an Embedded pointer (choose_cursor_mode's new ladder); the capturer pool now also keys on that mode. The virtual-output source passes false — its capture embeds the pointer where it can. The per-arm warns in vulkan_video (RGB-direct, native-NV12) are now structurally unreachable and removed. open_video's post-open check stays as the single backstop for what planning cannot see: a Vulkan-open falling back to VAAPI mid-session, and the gamescope residual (no embedded mode exists there, so a never-blending backend — H.264-on-AMD VAAPI, software — still streams cursorless; fixing that needs a compositing stage, deliberately not built in this pass). Zero-copy is preserved throughout — every fallback is a capture-negotiation change, never a readback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
759fdf767c |
fix(pf-capture): buffer-geometry guards + a genuine drop-oldest hand-off (sweep 2.4, 2.6)
**2.4 — buffer geometry correctness on the Linux CPU path.**
L5: the self-mmap de-pad mapped the fd from offset 0 and then indexed by `chunk.offset()` ALONE,
ignoring `spa_data.mapoffset` — where that spa_data's region actually begins inside the fd. For a
pooled-memfd producer (every buffer a slice of one fd) it therefore read the WRONG buffer, and the
`needed > avail` guard could not catch it: `avail` came from the whole-fd mapping, so it was ample
for any single buffer's span. PipeWire's own MAP_BUFFERS slice (the fallback) was always correct
because it already starts at `mapoffset` — only the hand-rolled "fix" path was wrong, so the two
paths now compute their base separately (checked add; both halves are producer-controlled).
L15: when `fstat` failed, the mmap length was invented as `offset + needed` — producer-controlled
geometry. That defeats the entire point of the fstat (the "buffer smaller than the frame span"
guard then compares against the number the producer just supplied) and can map, and read, past the
end of the object — a SIGBUS, not an `Err`. Without a real length we now decline to self-map and
let PipeWire's own slice serve, which is bounded by construction.
L12: `bytes_per_pixel()`'s catch-all answers 4 for NV12, so a producer-native NV12 buffer computed
`row = 4w` against a stride of ~w and ALWAYS tripped the stride guard — reporting "chunk stride <
row", i.e. blaming the producer for a host limitation. NV12 cannot be de-padded on this path at all
(plane 1 is not even in `datas[0]`'s span), so reject it with a message that says which side is at
fault and under what condition the NV12 offer is valid.
L14: `spa_meta_bitmap` was read field-by-field through an aligned `*const` at a
PRODUCER-controlled offset, with a SAFETY comment asserting an alignment the code never
established. One `read_unaligned` of the `Copy` POD instead.
**2.6 — hand-off semantics (L9, L10).** The frame channel was `sync_channel(8)` + `try_send`, i.e.
drop-NEWEST, while the trait documented drop-oldest. Neither remedy the plan offered works: a
`SyncSender` cannot pop, so "drain one and retry" is not expressible, and shrinking the bound makes
staleness WORSE, not better (with bound N the queue holds the first N frames of a stall and
everything after is discarded, so a larger N actually yields a fresher frame — the defect is the
discarding, not the depth). Replaced with a one-deep OVERWRITING mailbox plus a depth-1 wakeup
channel: publishing overwrites, so a stalled consumer loses the intermediate frames and is always
handed the freshest one. `next_frame` used to return the OLDEST of eight stale frames.
Falls out of it:
- `wait_arrival` now honours its documented "must NOT consume" contract by peeking the slot. The
`pending` field existed only to stash a frame the un-peekable channel forced it to consume;
it is gone.
- a queued `CapturedFrame` can own a dup'd dmabuf fd or a CUDA buffer, so the old 8-deep backlog
could pin eight compositor buffers. One-deep by construction now.
- L10: `set_active(false)` flushes the mailbox, so a reused capturer no longer opens the next
stream with the previous session's last frame (wrong content, and a `pts_ns` from the old
clock). Two lines, where flushing a queue + a `pending` stash was fiddlier.
- the producer-liveness signal moves to the wakeup channel's `Disconnected`, and a final frame
left in the slot as the thread exits is still served before the error.
pf-capture 13/13; workspace clippy --all-targets clean on Linux and windows-msvc.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
f495b201e1 |
fix(encode): delete the write-only EncoderCaps::supports_hdr_metadata
A caps field nothing reads is a contract nobody honors — and this one shipped write-only: its single reader anywhere in the workspace was a hardware-gated assertion inside pf-encode's own AMF smoke test. Both planes send the static HDR grade out-of-band unconditionally (the native 0xCE datagram per keyframe, the GameStream 0x010e control message), every first-party client reads exclusively that path, and none parse in-band SEI — so the host decision the field was reserved for (suppress out-of-band when the encoder embeds) can never validly exist. The field's doc contract had also rotted in two directions: it claimed set_hdr_meta no-ops when false (native AMF and QSV consume it regardless) and that only Windows direct-NVENC attaches in-band metadata (AMF and QSV do too). The in-band SEI/OBU emission itself is untouched — it stays a bonus for stock decoders, documented at the emit sites; the trait docs now describe the real routing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
17eb8100a3 |
fix(pf-capture): portal teardown, observable death, one negotiation resolver (sweep 2.1–2.3)
**2.1 (L1) — the portal thread leaked a live screencast session.** It parked on
`future::pending()` and `Drop` stopped only the PipeWire thread, so every dropped portal capturer
permanently leaked a thread + a 2-worker tokio runtime + a zbus connection — and because dropping
that connection is what ends the compositor's cast (ashpd's `Session` has no `Drop`), the
COMPOSITOR-side session leaked too. GameStream drops the pooled capturer on every HDR↔SDR
reconnect flip and opens a fresh one, so the sessions accumulated: exactly the "second conflicting
screencast" the pooling exists to prevent. Both portal threads now park on a `oneshot::Receiver`
and `PortalSession::drop` fires it, then waits — BOUNDED (750 ms, then detach with a warning) —
for a completion signal the thread sends after its runtime has been dropped. Bounded because
teardown runs on the session path and the thread may be inside a D-Bus round-trip: an unbounded
`join()` there would hang the host rather than leak. Every early-return path in `open` drops the
session too, so a failed PipeWire spawn no longer strands a fresh cast.
**2.2 (L2) — a dead capturer was pooled forever.** All three of the portal backend's terminal
states are sticky and observable only by consuming a frame, and `Capturer` exposed no health
predicate, so GameStream re-pooled unconditionally — after `result` was already an `Err`. One
zerocopy-worker death or one compositor restart therefore wedged GameStream portal video at 10 s
per reconnect attempt, permanently (that path has no rebuild closure). Adds
`Capturer::is_alive()` (default `true`), implemented for `PortalCapturer` as
`!broken && streaming && !thread.is_finished()` — the third term catches a dead PipeWire thread,
which is otherwise indistinguishable from an idle desktop. A static desktop stays `Streaming`, so
an idle-but-healthy capture is never reported dead. The host re-pools only on
`result.is_ok() && capturer.is_alive()` and logs which of the two retired it. Both halves ship
together: either alone leaves the wedge reachable.
**2.3 (L3/F1) — a "VAAPI" downgrade latch was really a global zero-copy kill switch.**
`spawn_pipewire` hand-mirrored the thread's `vaapi_passthrough` decision and the copy omitted
`raw_dmabuf_import_disabled`, so once that latch fired the capturer still believed it had made the
passthrough offer while the thread had already fallen back — and its timeout branch then latched a
downgrade for an offer nobody made. That downgrade was `note_vaapi_dmabuf_failed`, which fed
`pf_zerocopy::enabled()`, so ONE failed negotiation dropped every later session on the host — the
NVENC EGL→CUDA path included — to CPU capture until restart. And since the raw-passthrough offer
is also the PyroWave one on ANY vendor, a single PyroWave negotiation timeout was enough to do it
on an NVIDIA box.
Two fixes, both structural:
- `negotiation_plan(NegotiationInputs) -> NegotiationPlan` is now the ONE resolver, consumed by
the thread AND by `spawn_pipewire` — the mirror is deleted, not re-synced (WP7.6's shape), so
the drift class is unrepresentable. It is pure (every env/latch read is an input), which is
what makes it testable; the runtime-dependent half stays a method (`want_dmabuf` needs the
modifier list the importer's construction actually yielded). The redundant `importer.is_none()`
term goes: `build_importer` already excludes the passthrough, and that term is what made the
decision look impure enough to "need" a mirror. `PUNKTFUNK_FORCE_SHM` was ALSO read in both
places; now once.
- the capture-side downgrade becomes `pf_zerocopy::note_raw_dmabuf_negotiation_failed`, which
latches `RAW_DMABUF_DISABLED` — gating only the raw-passthrough decision. `enabled()` is now
the env var alone, and `VAAPI_DMABUF_FAILED` is deleted.
Also (L13, needed by 2.3's plan inputs): `PUNKTFUNK_PIPEWIRE_NV12` honoured only the exact string
`"0"`, so `"0 "` or `"false"` read as force-ON — the bug class of
|
||
|
|
232b6d6be2 |
test(encode/vaapi): extract the open-time decision logic and pin it with unit tests
The fallback backend under Vulkan Video — and the only AMD/Intel H.264 and 10-bit/HDR encoder — had 1,300 lines, 26 unsafe blocks, and zero tests. The device-free decisions now live in named functions with their contracts pinned: the entrypoint ladder + LP_MODE latch round-trip (the cross-GPU session-killer and the 8-bit-pins-10-bit under-advertisement are both key'd tests now), the PUNKTFUNK_VAAPI_LOW_POWER / _ASYNC_DEPTH grammars, the VUI ↔ scale_vaapi colour agreement (the Mesa-BT.601 hue-shift pin), the honest-downgrade depth table, the HEVC-Main10-only explicit profile, and the 10-bit probe gate. Probe + CPU-path encode round-trip ride along as #[ignore]d hardware smokes in the house style. No FFI plumbing was chased; no behavior changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
54a37aeb46 |
docs(pf-capture): truth pass over comments, docs and log strings (sweep Phase 1)
Every item here is prose that actively misdirects a maintainer or an operator. Landing it before
the defect fixes means those diffs get reviewed against accurate comments.
1.1 (X5) — split two merged doc blocks that documented the item ABOVE them: `hdr_meta`'s whole
contract was glued onto `fn cursor()` in lib.rs (leaving `hdr_meta` undocumented), and
`hdr_p010_selftest_at`'s was glued onto `hdr_p010_convert_bars_on_luid` in dxgi.rs. rustdoc for
the trait's central HDR contract was simply wrong.
1.2 (L9 doc half) — the `Capturer` trait doc advertised a "bounded drop-oldest channel". The
Linux portal's `sync_channel(8)` + `try_send` is drop-NEWEST: under a consumer stall the arriving
frame is discarded and the encoder gets the stalest queued one. Say so. (Phase 2.6 changes the
behaviour; this commit only stops the doc from lying about today's code.)
1.3 — delete the DONT_FIXATE claim from the dmabuf offer comment. `build_dmabuf_format` emits a
plain MANDATORY `ChoiceEnum::Enum` and `param_changed` re-emits nothing, so there is no two-step
DMA-BUF handshake here; libspa 0.9's `ChoiceFlags` cannot even express DONT_FIXATE. Recorded as a
real follow-up instead of an implemented thing.
1.4 — `mainloop.run()` no longer "blocks until process exit": the quit channel attached above it
stops the loop from `PortalCapturer::drop`.
1.5 (L11) — the 6-arm negotiation log ladder told a fully zero-copy PyroWave session "VAAPI
encode with the CPU capture path — zero-copy was disabled", one line after logging that it had
advertised the PyroWave device's dmabuf modifiers: the passthrough arm was gated on
`pyrowave_modifiers.is_empty()`, so the pyrowave case fell through to the CPU warning. Ungate it;
the CPU-path arm is now reachable only when no dmabuf is advertised at all, which is what it
claims. Its parenthetical also gains the third real cause (the session asked for CPU frames).
Control-flow-neutral for every other combination.
1.6 (W13) — `kick_dwm_compose` claimed a "sub-millisecond" round trip while its
cursor-on-a-sibling-display branch sleeps 35 ms on the CALLER's thread. State the cost on the
function and at both call sites, including which one is on the live frame path.
1.7 (W16) — DDA-era residue, DDA having been removed:
- retarget the win32u hook's rationale + all four log strings: its job is no longer "keep DDA on
one adapter" but "keep the virtual display on the adapter SET_RENDER_ADAPTER pinned" (a DXGI
reparent surfaces as the driver's TEX_FAIL render-adapter mismatch). The hook stays installed.
- the per-monitor-v2 DPI awareness rationale likewise: not DuplicateOutput1's E_ACCESSDENIED any
more, but keeping cursor/window coordinates in the PHYSICAL pixels the host's CCD geometry
(`source_desktop_rect`, `desktop_bounds`) is already in.
- `HYBRID_HOOK_HITS` was write-only. Surface it as `hybrid_hook_hits()` on the IDD-push open
line, which is the first point where DXGI has actually been exercised — the patch-readback
check proves the bytes landed, only this proves DXGI reaches the export.
- delete `hdr_p010_selftest()`: an unreachable 64×64 wrapper (main.rs calls
`hdr_p010_selftest_at`); its description moves onto the function that survives.
- `VideoConverter`'s docs promised a P010/BT.2020 output and a live scRGB input path. It pins
`YCBCR_STUDIO_G22_LEFT_P709` unconditionally and its only caller always passes
`scrgb_input: false`.
1.8 (X7) — the native handshake's 4:4:4 gate comment stated the OPPOSITE of what
`pf_capture::capturer_supports_444` returns on Windows ("delivers subsampled NV12/P010 today, so
it returns false there" — it forwards `resolved_backend_ingests_rgb_444()` and the IDD ring passes
BGRA through for an SDR 4:4:4 session).
No `.rs` behaviour delta: comments, doc comments and log strings, plus 1.5's control-flow-neutral
ladder and 1.7's counter accessor. Linux tests 6/6; clippy --all-targets clean on both targets.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
cc848479c4 |
test(pf-encode): re-point the cpu_img size-change smoke at the CSC guard's contract
arch / build-publish (push) Failing after 58s
ci / web (push) Successful in 51s
ci / docs-site (push) Successful in 53s
android / android (push) Failing after 7m59s
ci / bench (push) Failing after 4m9s
ci / rust-arm64 (push) Failing after 6m44s
ci / rust (push) Failing after 6m45s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
decky / build-publish (push) Successful in 24s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 47s
apple / swift (push) Successful in 5m28s
deb / build-publish (push) Successful in 9m25s
windows-host / package (push) Successful in 10m44s
docker / deploy-docs (push) Successful in 31s
docker / build-push-arm64cross (push) Successful in 5m33s
deb / build-publish-client-arm64 (push) Successful in 8m20s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 7m41s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 7m4s
deb / build-publish-host (push) Successful in 12m7s
apple / screenshots (push) Successful in 25m11s
vulkan_cpu_img_survives_a_source_size_change drove MISMATCHED source sizes through the then-lenient CSC arm as its vehicle for the staging cache hazard (format-only-keyed cpu_img → OOB copy while submit said Ok). e3354b6d's guard — correctly the equality check every sibling arm always had, against the MODE, not the coded extent, so the padded render-vs-coded tolerance in the direct arms is untouched — makes that scenario unrepresentable through submit and broke the test on main (.25 layers baseline read 13/13 instead of 14/12). Replaced by vulkan_csc_refuses_a_mismatched_source: refusal pinned in BOTH directions, plus the property that actually needs proving — a refused submit does not WEDGE the session (the bail lands after step 1's frame-type bookkeeping; the next well-sized frame must still encode, and an AU must come out). Verified on the 780M under validation layers: 8/8 vulkan tests, full-suite baseline restored to 14/12. The WP4.2 size-keyed staging stays as belt-and-braces; the hazard it fixed is now structurally unreachable through submit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6a2a153c0b |
chore(pf-capture): make the crate verifiable — Windows CI lint + two denies (sweep Phase 0)
Gate for the rest of design/pf-capture-sweep.md: today half this crate is compiled by no CI job
at all, so the Windows-side findings can't even be type-checked.
0.1 (X1) — windows-host.yml lints `-p pf-capture --all-targets`. The host lint above it builds
pf-capture only as a DEPENDENCY, so its `#[cfg(test)]` modules were never compiled anywhere: the
5 StallWatch tests, the DXGI HDR self-tests and the cursor-conversion tables were dead code in
CI. The workflow's own comment already diagnosed this blind spot and fixed it for pf-encode;
pf-capture never got the same treatment. No features on this crate, so no feature juggling and
no extra dep tree.
0.2 (X2) — `#![deny(unsafe_op_in_unsafe_fn)]` beside the existing
`deny(clippy::undocumented_unsafe_blocks)`. The crate declares "every unsafe block carries a
SAFETY proof; enforce it" in ten file headers, but in edition 2021 that lint has nothing to fire
on inside an `unsafe fn` — so the hardest FFI in the crate was exempt from its own program: the
ring/slot construction, the fence signal, the blend scratch, and every D3D converter ctor/convert.
119 operations across 16 functions now carry a proof. `shared_object_sa` loses its `unsafe`
marker instead (it states no precondition — only its RESULT is a raw pointer, which is the
caller's business).
0.3 (X4) — drop the crate-wide `#![allow(dead_code)]`. Verified: zero warnings on either target
without it, so it was hiding nothing the lint can see (W16's dead items are `pub`/written-once,
so they need Phase 1.7's deletion instead).
No behaviour change: only lint attributes, `unsafe { }` scoping and comments.
Linux `cargo test -p pf-capture` 6/6, clippy --all-targets clean on both targets (Windows
verified by cross-checking x86_64-pc-windows-msvc locally).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
bf9386ecb2 |
test(pf-encode): pin the typed-EINVAL classifier's chain-survival contract (Phase 8)
ci / docs-site (push) Failing after 52s
android / android (push) Failing after 6m28s
ci / web (push) Successful in 1m5s
ci / bench (push) Successful in 6m40s
decky / build-publish (push) Successful in 26s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
arch / build-publish (push) Successful in 12m51s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
ci / rust-arm64 (push) Successful in 9m51s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Failing after 42s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 17s
deb / build-publish-client-arm64 (push) Successful in 8m54s
deb / build-publish-host (push) Successful in 10m31s
deb / build-publish (push) Successful in 11m17s
windows-host / package (push) Successful in 10m41s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6m25s
docker / build-push-arm64cross (push) Skipped
docker / deploy-docs (push) Skipped
apple / swift (push) Successful in 5m25s
ci / rust (push) Successful in 22m21s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m7s
apple / screenshots (push) Successful in 23m50s
Rides on
|
||
|
|
bf9fb3fb22 |
fix(pf-encode): enable VK_EXT_queue_family_foreign for the dmabuf acquires (Phase 8)
Both Linux Vulkan encode backends named QUEUE_FAMILY_FOREIGN_EXT as the acquire barriers' src family without ever enabling the extension — spec-invalid on every device, tolerated by RADV. The audit filed vulkan_video's three sites; pyrowave's fresh-import acquire had the identical defect on its own device (critic catch). Enable when advertised (a fresh open-time enumerate — the rgb probe's is a probe-local and skipped entirely on native-NV12, so there was nothing to reuse; pf-presenter/dmabuf.rs is the in-repo precedent that already enables this extension). Not advertised → the core-1.1 QUEUE_FAMILY_EXTERNAL conservative substitute, chosen once at open and warn-logged (no fleet hardware takes that arm; such devices were never valid targets before). All four sites are acquire-only (src=FOREIGN, EXCLUSIVE images, oldLayout=UNDEFINED) — the swap is index-only. On-glass: 780M under validation layers — vulkan smokes + pyrowave smokes green, FOREIGN advertised and enabled, no fallback engaged. Shared ext_advertised helper in vk_util (cfg = the union of both consumers) with a unit test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1cefd37603 |
fix(pf-encode): arbitrate NVENC split-encode vs sub-frame readback (Phase 8)
Verified against nvEncodeAPI.h's own splitEncodeMode doc (user-prompted — the audit's 'exclusive for HEVC' one-liner deserved checking): - H.264: split 'is not applicable' — hard-DISABLE the mode so the written config, CeilingKey, the split diagnostic log and the rejection-retry stay truthful (the retry used to re-open a byte-identical session after an H.264 'split rejection'). The libav path's operator arm gains the codec gate its auto arm always had. - HEVC: split 'not supported if … subframe mode' — when WE force split (TWO/THREE/AUTO_FORCED, the 4K120 throughput lever), sub-frame yields with a logged escape (PUNKTFUNK_SPLIT_ENCODE=0 chooses sub-frame). ⚠ Keyed on FORCED modes only, never != DISABLE: AUTO(0) is the resolver's fallthrough for every sub-950Mpix session, and the wider key would have disarmed the Phase-3 chunked-poll feature fleet-wide (critic catch). Under AUTO the driver arbitrates — the shipped state. - AV1: untouched — per-tile sub-frame + split are legal together. The arbitration is a pure nvenc_core fn called by each backend BEFORE the ladder, the ceiling key and the chunked-poll latch — all three see the post-arbitration truth. A drop inside build_init_params would have left poll_chunk busy-polling its whole budget every AU (numSlices stays 0 without reportSliceOffsets; both loop exits dead — critic catch). Linux latches subframe_forced beside subframe_on at query_caps (no env re-reads after open); Windows records the arbitrated state so reconfigure presents exactly the params the open had (also closes the pre-existing mid-session env-flip hazard there). Truth-table tests in nvenc_core; PUNKTFUNK_NVENC_SUBFRAME documented (it never was); PUNKTFUNK_SPLIT_ENCODE row updated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
25765c53ec |
fix(encode): classify libav-NVENC open failures by errno, not English strerror text
ci / docs-site (push) Successful in 58s
ci / web (push) Successful in 59s
apple / swift (push) Successful in 5m17s
ci / bench (push) Successful in 7m27s
deb / build-publish (push) Failing after 8m0s
ci / rust-arm64 (push) Failing after 9m1s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
decky / build-publish (push) Successful in 22s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 13s
ci / rust (push) Failing after 9m31s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
deb / build-publish-host (push) Successful in 10m23s
arch / build-publish (push) Successful in 12m54s
android / android (push) Successful in 15m8s
deb / build-publish-client-arm64 (push) Successful in 7m54s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 5m43s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 6m35s
windows-host / package (push) Successful in 19m10s
docker / build-push-arm64cross (push) Successful in 8s
docker / deploy-docs (push) Successful in 24s
apple / screenshots (push) Successful in 23m39s
The bitrate-probe ladder stepped down on format!("{e:#}").contains(
"Invalid argument") — an English substring over the WHOLE context chain,
which also fired on any other wrapped EINVAL (e.g. a CUDA-context errno)
and gated a ~10-step ladder on strerror wording. The root ffmpeg::Error
survives the anyhow chain; downcast and match Error::Other{errno:EINVAL}
instead. Same fix for the intra-refresh ENOSYS probe in the open path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
36589c39da |
fix(gamestream): stall-recovery ladder — a wedged encoder no longer ends the stream
The backends deliberately turn a wedged GPU into a bounded error so the caller can reset in place, but only the native plane ever did: on the GameStream path every submit/poll error propagated straight out of the video loop, costing Moonlight clients a full disconnect/reconnect. Port the native ladder (bounded resets + backoff + silent-wedge watchdog, same field-lesson backoff curve). Also keep the drop-recovery keyframe armed until it actually EMITS: it was consumed on read, so the coalesce gate could swallow it for good — leaving duplicate wire indices in the encoder's reference table for a later RFI to anchor on, exactly the stale-anchor case rfi.rs exists to prevent, and it opened only under congestion + loss, which is when RFI fires. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e3354b6d5d |
fix(encode/vulkan): guard the CSC source dimensions, and bound reset()'s wait
The CSC path was the only backend arm that took frame.width/height on trust: the shader samples with clamped 1:1 texelFetch, so a mismatched frame silently streamed a cropped/edge-padded picture where every sibling errors into the encoder-rebuild path. The import cache now also carries the extent it imported at (a (st_dev, st_ino) hit alone doesn't prove the allocation still matches) and is dropped on reset(). reset() opened with an untimed device_wait_idle on the one thread whose every other wait is capped at ENCODE_FENCE_TIMEOUT_NS for exactly this reason — reset() runs BECAUSE the GPU looks wedged. Both vulkan-video and pyrowave now bound the wait and report "no in-place rebuild" on timeout instead of parking recovery on the suspect device; Drop keeps the unbounded wait (teardown must stay memory-safe against a wedged device). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
28f8fc71c4 |
refactor(pf-encode): split vulkan_video's construction tail into vk_build.rs (WP7.5)
ci / web (push) Successful in 56s
ci / rust-arm64 (push) Failing after 2m18s
ci / docs-site (push) Successful in 1m2s
android / android (push) Failing after 4m7s
decky / build-publish (push) Successful in 20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
apple / swift (push) Successful in 5m21s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
ci / bench (push) Successful in 6m10s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 5m40s
deb / build-publish-client-arm64 (push) Successful in 7m57s
docker / build-push-arm64cross (push) Successful in 10s
deb / build-publish (push) Successful in 10m6s
docker / deploy-docs (push) Successful in 24s
deb / build-publish-host (push) Successful in 10m30s
arch / build-publish (push) Successful in 13m57s
windows-host / package (push) Successful in 14m6s
ci / rust (push) Successful in 22m9s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m30s
apple / screenshots (push) Successful in 24m30s
The ~820-line tail of free builders — make_frame/make_frame_csc/ make_frame_common, make_video_image, probe_rgb_direct, the H.265/AV1 parameter-set writers and the AV1 bit-writer — moves to a #[path] child module, the amf_sys.rs shape: the child sees the parent's private items (Frame and friends), so the split costs zero visibility churn. Six parent-called items went pub(super); five stay child-private (dead_code is per-item and each is used within the child). vulkan_video.rs drops 5,292 → 4,489 lines and the construction unsafe gets its own review surface; steady-state encode logic stays in the parent. ⚠ Trap recorded for future child-module splits: inline `use super::X` statements INSIDE moved fn bodies silently change meaning (super shifts one level) — vk_av1_encode/vk_valve_rgb imports needed crate:: paths. Proven on-glass, not just compiled: all 8 vulkan GPU smokes green under the validation layers on the 780M post-split (H.265 + AV1, RGB-direct, CSC, CPU paths — every moved constructor exercised). nvenc_cuda.rs and qsv.rs are DECLINED the same treatment, with evidence in the handoff doc: no equivalent self-contained seam — their candidate regions are ~150-line loader/accessor clusters interleaved with the encoders' own state types, and a thin-forwarder impl split is exactly the churn a no-defect phase penalizes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bc5ec5105c |
refactor(pf-encode): one Linux backend resolver, consumed by dispatch AND mirrors (WP7.6)
The Linux backend decision existed as open_video_backend's string match plus five partial hand-copies (the zero-copy-plane gate, the pyro advertisement gate, the software advertisement pin, the vulkan pref ceiling, resolved_backend_is_gpu). Windows solved this long ago — windows_resolved_backend() is consumed by its dispatch AND its mirrors, with labels still stamped at the open sites. Linux now has the twin: resolve_linux_backend (pure, lazy auto probe) + linux_resolved_backend (config wrapper, unknown→auto exactly as every mirror's old `_` arm). ⚠ This deliberately DEVIATES from the audit's shadow-assertion prescription, on both critics' findings: the shadow had no execution venue (open_video had ZERO test call sites; shipped hosts are --release) — unfalsifiable ceremony — and the file's own Windows half proves dispatch-consumes-resolver is safe: the mgmt record is protected by the label-from-the-open-site convention, not by resolver avoidance. One consumed table beats two tables plus an inert cross-check. Preservation riders from the critique, all applied: - pref_ceiling KEEPS its cfg!(vulkan-encode) branch (the resolver is feature-blind; dropping it re-creates advertise-then-die-at-open). - linux_zero_copy_is_vaapi's Vulkan|Software arm preserves the old `_` fallthrough EXACTLY — the vulkan-on-NVIDIA capture-plane mismatch and the software twin are FILED in the design doc, not fixed here. - resolved_backend_is_gpu splits as linux + not(any(windows, linux)) — never a target list, so no exotic target loses the fn. - The auto probe is lazy (impl FnOnce) — explicit prefs stay zero-probe (/serverinfo polls through these mirrors), pinned by a panicking closure in the resolver test. New coverage: the full alias table pinned, and a GPU-free dispatch test through the real open_video_backend (software arm, vendored openh264) — its first test call site anywhere. Config-latch seam guarded loudly. Cross-crate mirrors recorded out of scope in the design doc: session_plan::resolve_encoder, gamestream/serverinfo.rs base_codec_mode_support, capture.rs's "pyrowave" string. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
cb69cd3b8c |
refactor(pf-encode): move the AMF C-ABI mirror to its own file (WP7.4)
The ~410-line hand-mirrored AMF vtable ABI was already isolated in an inline `mod sys` — the remaining value of the audit's 'best split in the crate' is the FILE boundary: amf.rs drops 3379 → 2965 lines and the pure unsafe-FFI surface (25 unsafe fns, zero policy) is reviewable in isolation, which is the crate's stated review goal. Pure move: `#[path = "amf_sys.rs"] mod sys;` keeps the module name and every `sys::` call site byte-identical; the module was self-contained (only `use std::ffi::c_void`, no super:: references). The banner became the file's module docs; contents de-indented one level — rustfmt-clean on the first check, which is what proves the move byte-exact. Gated on .173: all five clippy combos + the amf-qsv,qsv test leg (38 passed — the live AMF matrix on real VCN silicon among them). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9491cc8759 |
refactor(pf-encode): extract the range-family RFI recovery policy (WP7.2)
The two direct-NVENC backends carried hand-copied twins of the same loss-recovery decision: range validity, covering-range dedup, DPB window, clamp — ~30 duplicated lines each. The decision now lives once as nvenc_core::plan_range_recovery (the range half of WP7.2; the slot half is enc/rfi.rs), pure and unit-tested; each backend keeps its session gate, its unsafe per-timestamp driver loop, and its state stores. The step order is load-bearing and now pinned by tests: the covering dedup runs with the UNCLAMPED last and BEFORE the DPB window (a covered re-ask never touches the driver even when the range has since aged out of the DPB), the boundary at next_ts - RFI_DPB is inclusive, and the Invalidate carries the CLAMPED last — which is also what the caller records in last_rfi_range, exactly as the inline code stored it. A driver failure mid-loop still returns false with NO range recorded and no anchor armed. Decline deliberately clears nothing (neither twin touched pending_anchor on decline — same shape as Vulkan's non-clear, opposite of AMF/QSV; do not harmonize). The exact-cover → Covered test records EXISTING behavior including that a covered range survives a forced IDR with zero driver calls — a recorded fact, not an endorsement. RFI_DPB's import leaves both twins: its only per-backend use was the arithmetic that moved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9f1e648e4e |
refactor(pf-encode): extract the slot-family RFI recovery policy (WP7.2)
AMF (user-LTR bitfield), QSV (mfxExtRefListCtrl) and Vulkan Video (the
app-owned DPB slot table) each hand-implemented the same loss-recovery
decision: distrust every reference encoded at-or-after the loss start,
anchor on the newest one strictly older. Three copies had already
diverged once — the
|
||
|
|
d28fb1282b |
test(pf-encode): guard TrackedEncoder's forwarding completeness (WP7.7, cheap half)
A defaulted Encoder method that TrackedEncoder doesn't forward silently no-ops through the wrapper — the host loop only ever holds the wrapped box, so the feature dies for every session with nothing in the logs. The trap has bitten three times (set_wire_chunking's §4.4 chunking probe, set_pipelined's LN3 escalation, applied_bitrate_bps's ABR truth), and every Phase 7 consolidation that adds a trait method re-arms it. Source-text parse of the trait and the forwarding impl (both top-level rustfmt items: block ends at the first column-0 brace, method names sit on 'fn '-prefixed lines), then set equality — the reverse direction is already a compile error, so equality == completeness. Mutation-verified: removing set_wire_chunking's forward fails naming exactly that method. 16/16 today. Limit stated in the test doc: name-set equality only — a forward whose body delegates to the WRONG inner method still passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
94b818da28 |
fix(client/pyrowave): declare the decode planes' real format — 10-bit sessions decoded through an R8 view of R16 planes
apple / swift (push) Successful in 5m14s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 6m32s
arch / build-publish (push) Failing after 7m6s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
decky / build-publish (push) Successful in 18s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m53s
android / android (push) Successful in 12m56s
docker / deploy-docs (push) Successful in 24s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m41s
docker / build-push-arm64cross (push) Successful in 4m15s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m40s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 7m44s
deb / build-publish-host (push) Successful in 10m43s
deb / build-publish-client-arm64 (push) Successful in 9m8s
apple / screenshots (push) Successful in 22m58s
flatpak / build-publish (push) Successful in 6m56s
deb / build-publish (push) Successful in 8m58s
ci / rust (push) Successful in 22m10s
ci / rust-arm64 (push) Successful in 9m22s
ci / web (push) Successful in 48s
ci / docs-site (push) Successful in 1m13s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 12m48s
ci / bench (push) Successful in 6m3s
The HDR/4:4:4 leg (
|
||
|
|
3e7828522d |
fix(tray): drop the conflicting-host warning — the tray is the wrong place for it
ci / web (push) Successful in 59s
ci / docs-site (push) Successful in 1m6s
ci / bench (push) Successful in 7m0s
deb / build-publish (push) Failing after 7m35s
decky / build-publish (push) Successful in 22s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
deb / build-publish-host (push) Failing after 8m6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
android / android (push) Successful in 13m38s
ci / rust-arm64 (push) Successful in 12m28s
docker / deploy-docs (push) Successful in 24s
arch / build-publish (push) Successful in 14m10s
windows-host / package (push) Successful in 10m56s
deb / build-publish-client-arm64 (push) Successful in 8m36s
docker / build-push-arm64cross (push) Successful in 4m37s
apple / swift (push) Successful in 5m28s
ci / rust (push) Successful in 22m58s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 13m9s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m0s
apple / screenshots (push) Successful in 23m51s
The tray led its tooltip with "⚠ conflicting host: Sunshine" and (on Linux) went NeedsAttention for a Sunshine that was merely *installed* — never started, never listening, harmless. An always-on warning over a dormant package is a false alarm on every poll, and the tray is the wrong surface for a one-shot install-time observation anyway. Removes the `conflicts` field from the tray's `Summary`, the tooltip prefix, `has_conflicts()`, and the ksni attention branch that fed on it. The host still detects and reports conflicts where that belongs — the startup `punktfunk::detect` warning, the `detect-conflicts` subcommand, and the `/api/v1/local/summary` field, which the tray must (and does, per the new test) keep tolerating on the wire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ffc7aec91a |
feat(encode/pyrowave): log which GPU pyrowave picked — the selection stays put, by decision
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 1m12s
apple / swift (push) Successful in 5m20s
ci / rust (push) Failing after 5m55s
android / android (push) Failing after 5m59s
deb / build-publish (push) Failing after 5m1s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
decky / build-publish (push) Successful in 17s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
ci / bench (push) Successful in 7m17s
ci / rust-arm64 (push) Successful in 9m40s
docker / deploy-docs (push) Successful in 26s
docker / build-push-arm64cross (push) Successful in 4m9s
deb / build-publish-host (push) Successful in 10m32s
arch / build-publish (push) Failing after 12m34s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 5m58s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 5m50s
deb / build-publish-client-arm64 (push) Failing after 7m10s
windows-host / package (push) Successful in 17m27s
apple / screenshots (push) Successful in 23m26s
WP4.5's device-selection half, closed as the observability intermediate after TWO selection designs died in adversarial review: - Attempt #1 (d26bcf05, withdrawn): match pf_gpu::selected_gpu(). Its Linux auto arm answers "the NVIDIA GPU" whenever /dev/nvidiactl exists, moving the encoder off the iGPU that can import the compositor's dmabufs on an Intel-compositor + NVIDIA-present laptop — import failures feed the process-wide raw-dmabuf latch, which never un-latches. - Attempt #2 (this session, withdrawn before commit): anchor on the PUNKTFUNK_RENDER_NODE-else-renderD128 node via VK_EXT_physical_device_drm. Render minors are driver-BIND-ORDER artifacts, not display topology: on the common AMD-iGPU + NVIDIA-display desktop, in-tree amdgpu binds before out-of-tree nvidia, so the anchor deterministically picks the idle iGPU while the compositor allocates on NVIDIA — the same latch, opposite polarity, behind a success-looking log. The correct oracle is evidence of which device ALLOCATED the capture buffers — producer identity from the capture negotiation, threaded per session into this open. Until that plumbing exists, selection stays first-usable, both call sites still share one selector (pure over the device list, so capture_modifiers and open_inner cannot diverge — including across an in-place resize's re-open, which does not renegotiate capture), and the open logs ONE greppable line: picked vendor/device, the anchor node and its owner (DRM render major/minor, VK_EXT_pci_bus_info fallback), and the console's selected GPU. A wrong-device session on a multi-GPU host used to be completely invisible; a field report can now show it. No WARN arm on purpose: the wrong-pick direction inverts between the laptop and desktop topologies, so a mismatch is not evidence of a wrong pick, and a warning that fires forever on healthy hosts teaches people to ignore warnings. Decision recorded against the audit's framing: manual console GPU selection stays unhonored by pyrowave on Linux (the Windows twin honors it) — honoring console-mutable state without per-session threading is what made attempt #1 unsafe. Verified on the 780M: the line resolves all three identities (1002:15bf x3, DRM-props match live); full pyrowave on-glass suite green; selection behaviour byte-for-byte unchanged. WP4.5 (device half). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
46935bf0a5 |
fix(encode/vulkan): VBR instead of CBR — the driver was stuffing ~98% filler into every calm stream
Second attempt at WP6.3; the first (ce543668) was withdrawn after its tight CBR window measured as a 36x bandwidth regression (97% filler NALs). The correction that unlocked this one: that measurement's BASELINE row was an 8-frame artifact. Under the shipped 1000ms/500ms CBR window a calm stream overflows the CPB once the initial fill drains (~30 frames at 10Mbps/60fps) and RADV then pads every frame to the exact rate share, forever. Measured on the 780M (1280x720@60, 10Mbps, calm content): 64 frames = 97.5% filler; 300 frames = 5.63MB at 98.5% filler where this commit ships 83KB at 0%. AV1: 99.6% -> 0%. The status quo was ~the full target bitrate of zeros on every idle AMD/Intel Vulkan-encode desktop and Steam Deck — and stuffing to exactly the target permanently satisfied the ABR calm brake (actual >= 3/4 * current), the ratchet WP6.3's withdrawal feared from the tight window, live in the shipped code all along. The fix reads VkVideoEncodeCapabilitiesKHR::rateControlModes (previously ignored — rateControlMode was hardcoded CBR with no capability check) and installs VBR with average == max plus the house ~1-frame window (vbv_window_ms, PUNKTFUNK_VBV_FRAMES-scaled) when the driver advertises VBR. VBR permits underspend, the exact missing degree of freedom: Vulkan exposes no filler-suppression control (AMF's filler_data=false / NVENC's default-off have no VK equivalent), so the MODE is the only lever. CBR-only drivers keep the loose window untouched — tightening it under CBR just starts the stuffing 30 frames earlier. Drivers advertising neither mode (ANV per current Mesa) keep the pre-existing CBR install, now WARN-logged. No pacing claim, deliberately: burst A/B on the 780M is byte-identical between 1000ms CBR and 17ms VBR (max AU 1.19MB in both) — this firmware ignores the window for QP decisions entirely. The payload is filler elimination. PUNKTFUNK_VULKAN_RC=cbr|vbr is the field escape hatch and the on-box A/B control (two withdrawn attempts bought that insurance). Also on the same caps struct: maxBitrate is now read and clamps open + retarget (RADV reports 1 Gbps — within 5% of the 4K120 ABR targets), and applied_bitrate_bps() reports the encoder-side truth (pending-first, so the session loop's read right after reconfigure_bitrate sees the clamp) — without it a binding clamp would feed the ABR a phantom base, the trap the trait doc names. And the one-frame VUID-vkCmdBeginVideoCodingKHR-pBeginInfo-08254 violation found in the withdrawal review: record_submit promoted a pending retarget into self.bitrate BEFORE recording whenever first_frame was set, so after a mid-stream reset() (which preserves the pending rate and rc_installed) the begin-coding declaration named a rate the session had not installed — and the two triggers, ABR retarget and the stall watchdog, correlate. Now the declaration always names the session's current rate and the RESET install carries the pending one via its own struct; promotion stays in post_submit_bookkeeping. The extended validation-layer test reproduces the retarget-then-reset coincidence: exactly one 08254 on the pre-fix build, zero on this one (RADV PHOENIX, on glass). Gates: docker amd64 legs green; Windows .173 seven legs (34 passed); .25 full-suite parity vs origin/main (identical CUDA-only failures) + all 9 vulkan on-glass tests + validation layers clean. WP6.3, plus WP7.1's ms-form half (vbv_window_ms). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fdded5b8c3 |
fix(encode/pyrowave): refuse a frame that isn't the session's mode
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 55s
ci / bench (push) Failing after 6m37s
ci / rust (push) Failing after 6m43s
deb / build-publish-host (push) Failing after 5m44s
arch / build-publish (push) Failing after 6m45s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
decky / build-publish (push) Successful in 18s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 7s
docker / build-push-arm64cross (push) Successful in 9s
docker / deploy-docs (push) Successful in 23s
ci / rust-arm64 (push) Successful in 9m47s
deb / build-publish (push) Successful in 9m9s
android / android (push) Successful in 11m59s
deb / build-publish-client-arm64 (push) Successful in 8m44s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 8m27s
windows-host / package (push) Successful in 17m35s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m58s
apple / swift (push) Successful in 6m51s
apple / screenshots (push) Successful in 26m43s
PyroWave never checked frame dimensions against the session, and it applies no alignment — `width`/`height` are the negotiated mode verbatim — so a mismatched frame was encoded edge-smeared or cropped, silently, forever. Every other Linux backend already refuses exactly this, with this shape, in `submit`: libav-NVENC (`linux/mod.rs`), VAAPI (`vaapi.rs`) and openh264 (`sw.rs`) all carry the same `ensure!`. PyroWave was the only one that didn't. That is the justification; an earlier draft cited `vulkan_video.rs` instead, which is the weaker precedent — it bails in its Dmabuf arms only, and its CSC path and CPU arm have no dimension guard at all. Mostly this is a wrong-picture bug and not a memory-safety one: `rgb2yuv.comp` clamps every fetch with `min(p, textureSize - 1)` and the CPU arm uploads `min(len, need)` into a session-sized image. But it also closes a narrow real hazard that was not in the filing: `import_cached` keys on `(st_dev, st_ino)` and returns the cached `VkImage` on a hit WITHOUT rechecking the extent, and unlike the capture side it is never cleared on a renegotiation — so a dmabuf inode recycled across a shrinking renegotiation would hand the encoder an image sized for the old, larger allocation. This check closes that route. ⚠ Recorded at the code because it changes the failure mode, not just the detection: a mismatch is NOT always transient. A compositor-initiated PipeWire renegotiation updates the capturer's size in place and signals nothing the encode loop reads, so it can be a permanent new steady state — and `reset()` reopens at the same dimensions by construction, so the host's five-reset budget cannot recover (~3.1 s of frozen stream, then the session ends) where before it would have streamed on with a wrong picture. For a real mode change that trade is clearly right; for a 16-row KWin mismatch it is not, and the proper fix is for the host to classify this error as a PIPELINE rebuild rather than an encoder reset. Filed, not done here. The device-selection half of WP4.5 was written, reviewed and WITHDRAWN — see the handoff doc. Matching the selected render GPU regresses the hybrid Intel-compositor + NVIDIA-present topology this project has a live field report for, because `selected_gpu()` answers NVIDIA whenever `/dev/nvidiactl` exists regardless of where capture actually runs. WP4.5 (dimension half). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9fe9cbbf07 |
perf(encode/vulkan): build the padded RGB frame in the staging memory, not beside it
The RGB-direct CPU-upload path allocated and zero-filled a whole padded frame on every submit, filled it row by row, then memcpy'd the whole thing into the mapped staging buffer. The zero-fill was entirely dead: the row loop writes every byte of every row, and rows past the source re-copy the last source row. So the frame paid for an allocation, a page-fault storm over fresh pages, a full zero-fill, and two full-frame copies where one would do. Map first, write the padded rows straight into the mapping. Nothing reads back from the destination — the row source is always the caller's buffer, never the staging memory — so writing into (write-combined) host memory costs nothing extra, and `make_host_buffer` allocates HOST_COHERENT, so the writes need no flush before the transfer reads them. Measured on real RDNA3 780M silicon, release build, `submit` alone, 400 frames x 3 rounds: - **1920x1080** (rows padded to 1088): p50 2315 -> **1725 us** (-25%), p99 2819 -> **1972 us** (-30%), and the spread (p99-min) tightens 739 -> 483 us. - **1366x768 -> 1408x768** (BOTH axes padded, so the column tail loop runs): p50 1013 -> **926 us** (-8%), p99 1303 -> **1110 us** (-15%). This is the one case where the new code could have been slower — 4-byte stores straight into write-combined memory — and it is not. - **CONTROL, 1280x720** (64x16-aligned, so the branch is never entered): p50 692 vs 692 us. No delta, which is what makes the two above attributable to this change rather than to anything else on the branch. The branch is reached by any RGB-direct session at a mode that is not 64x16 aligned, whenever capture delivers CPU frames (the default Linux capture path is dmabuf and never enters it). 1080p qualifies, since 1080 aligns to 1088. ⚠ An earlier draft of this message claimed "33 MB at 4K" — that is wrong: 3840 and 2160 are both already aligned, so 4K UHD never enters this branch at all. The large case is an ultrawide like 3440x1440 -> 3456x1440, ~20 MB. Three things this deliberately does NOT do: - The extent guards stay scoped to the `pad` branch. The CSC path deliberately supports a source SMALLER than the encode extent — its shader clamps at sample time — so a guard hoisted above the branch would break it. - Every fallible step stays above the map. An error raised between `map_memory` and `unmap_memory` would strand the mapping for the life of the slot's staging buffer, and the next frame's `map_memory` on it then violates VUID-vkMapMemory-memory-00678. (The filed "two exits above the map leak Vulkan objects" hazard is separately already gone: `47a23bec` moved that unwind into `make_host_buffer`.) - It adds guards rather than removing them: a zero source axis made `sh - 1` underflow, and "cannot fail after the map" has to be true by construction. Three corrections to an earlier draft, all found by review of that draft: - The `dw*dh*4 == need` precondition was a `debug_assert!` placed BELOW the map. That is wrong twice: it made the only check on the slice length vanish from the builds that ship, and a fired assert would have unwound past `unmap_memory` — the exact failure the bullet above says was designed out. It is now a real checked `?` above the map. - `need` was `(iw * ih * 4) as u64`: a u32 multiply widened after the fact, which agreed with the usize slice length only up to ~32768x32768. The old code's `min(need)` was a hard backstop against exactly that and the rewrite dropped it. Now widened before the multiply, matching `read_slot`'s existing discipline. - The extent guard now runs BEFORE the payload-length guard, so the usize `sw * sh * 4` cannot overflow on a garbage frame header. WP6.2(a). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e680096c6a |
perf(encode): stop re-reading the environment on every submit and poll
`std::env::var` was on three per-frame paths. Measured on `.173` (Windows, 57 environment variables, 2M iterations): **121.9 ns** per call for the NVENC in-flight cap and **114.9 ns** per call for the ffmpeg poll spin, against **0.9 ns** once memoized. On Linux, 32 ns → 1.5 ns. ⚠ Those numbers deflate the filing, and that is worth recording: the audit ranked this as a hot-path defect, but ~120 ns/frame is ~0.003% of a frame budget. The fix is still right — it is free, and it takes a global environment lock off the encode thread — but nobody should schedule it ahead of anything on the strength of the "hot path" framing. The severity RANKING was also inverted, and the measurement confirms why. The site the audit called worst — `nvenc_cuda`'s backpressure loop condition — costs a default session nothing, because the condition short-circuits on `async_rt.is_some()` and the default session never engages the two-thread retrieve. The one that actually pays every frame is Windows `submit`, which consults `async_inflight_cap()` in BOTH arms of the ring-depth match, sync mode included, where the result is then thrown away. Windows `poll` is the second unconditional one, and the audit ranked it last. Memoized inside each helper rather than latched into a session field. Nothing in the workspace mutates these variables at runtime (enumerated: no `set_var` for either key anywhere in first-party code, and the Windows service's arbitrary-key `host.env` loader runs in the SCM supervisor, which re-execs the host as a child and never opens an encoder itself). A field would instead change WHEN the value is read — and the Windows `cap` composes the env with `input_ring_depth`, which `set_input_ring_depth` may change after open, so freezing that half would reintroduce the in-place-overwrite bug the ring term exists to prevent. Two deliberate behaviour changes ride along on `PUNKTFUNK_FFWIN_POLL_MS`, so "behaviour-preserving" describes the memoization only, not this whole commit: - **A 1000 ms ceiling.** The reachable hazard was never the overflow — that needed `ms >= 1.8e16` — it was a slipped digit: `=100000000` was a 27.7-hour spin of the encode thread. - **`.trim()`, now on all three parsers.** An earlier draft applied the house rule (WP7.8) to one of the three, which left `PUNKTFUNK_NVENC_ASYNC=" 1 "` working while `PUNKTFUNK_NVENC_ASYNC_DEPTH=" 6 "` silently fell back to 4 — and memoization would have frozen that silent fallback for the process lifetime. Reachable: the Windows `host.env` loader trims around `=` before stripping quotes, so `=" 2 "` yields a value with inner spaces. ⚠ Correction to an earlier draft of this message, which asserted that the audit's `saturating_mul` proposal would relocate an overflow panic into release builds. That is FALSE and the code comment now says so: `Duration::from_micros(u64::MAX)` is ~1.8e13 seconds, six orders of magnitude below `Duration`'s ceiling, so `Instant + Duration` neither overflows nor panics (measured, with and without debug assertions). `saturating_mul` is still wrong, for a different reason — it sets a deadline ~584,000 years out on a spin that provably never produces the owed AU, i.e. it wedges the encode thread permanently. A hang, not a panic. WP6.1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0044649b19 |
fix(encode/nvenc): codec-gate the HEVC 4:4:4 union write, and stop it eating the 10-bit arm
android / android (push) Successful in 12m39s
windows-host / package (push) Successful in 10m31s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
decky / build-publish (push) Successful in 19s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
arch / build-publish (push) Successful in 18m50s
docker / deploy-docs (push) Successful in 12s
docker / build-push-arm64cross (push) Successful in 5m55s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m21s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m29s
ci / docs-site (push) Successful in 1m11s
ci / web (push) Successful in 1m11s
apple / swift (push) Successful in 5m39s
ci / bench (push) Successful in 6m55s
ci / rust-arm64 (push) Failing after 8m55s
ci / rust (push) Successful in 21m27s
deb / build-publish (push) Successful in 8m45s
deb / build-publish-client-arm64 (push) Successful in 9m9s
deb / build-publish-host (push) Successful in 11m26s
apple / screenshots (push) Successful in 26m31s
`encodeCodecConfig` is a C union, so the `hevcConfig` writes in the 4:4:4 branch are only meaningful on an HEVC session — on H.264 or AV1 they reinterpret that codec's own config bytes. The branch was gated purely on `chroma_444 && full_chroma_input` with no codec test, and stayed non-UB only because `lib.rs` degrades 4:4:4 for non-HEVC codecs: a two-file invariant with nothing asserting it, on the path BOTH direct-NVENC backends take. The audit filed that much. What it did not note is that the same shape hides a second bug: this is an `if`/`else if`, so a non-HEVC session that arrived with `chroma_444` set took the HEVC branch and skipped the per-codec bit-depth arm entirely — ending up with neither HEVC 4:4:4 (wrong for it) nor its own 10-bit configuration (simply absent). AV1 would have lost `pixelBitDepthMinus8`/`inputPixelBitDepthMinus8` silently. Non-HEVC now falls through to the arm that knows what to do with it, and an unexpected request is logged rather than swallowed. Adds two tests that need no GPU — `apply_low_latency_config` is pure config authoring, so an AV1 session can assert it never receives the HEVC FREXT profile GUID (an INVALID_PARAM at open) and that its own depth still lands, with an HEVC case guarding the good path. ⚠ `NV_ENC_CONFIG` must NOT be `mem::zeroed` in those tests: `frameFieldMode`/`mvPrecision` are C enums whose discriminants start at 1, so all-zero is not a valid value and Rust's zero-init check ABORTS the process (SIGABRT, caught by the Linux gate). They seed it from `Default` the same way `build_config` does before overwriting from the driver preset. Worth knowing before writing any further test against these SDK types. Verified: Linux gate L1-L4 green (39 tests, the two new ones among them) and the full Windows gate on .173 — 7 legs. Note the Windows test leg runs `--features qsv`, so these tests only execute on the Linux leg; the Windows legs prove the change compiles in all five feature combinations. |
||
|
|
b3f8803ea3 |
fix(encode/qsv): stop advertising intra-refresh the driver silently dropped
`ir_active` was `cfg.intra_refresh && set.co2.is_some()` — i.e. "we asked for it", not "we got it". Both `Query` and `Init` can return MFX_WRN_INCOMPATIBLE_VIDEO_PARAM, a WARNING the open path deliberately accepts, while dropping the intra-refresh wave on the floor. That lie is not cosmetic. `ir_active` feeds `EncoderCaps::intra_refresh`, so the session advertises gradual refresh to the client; the client then stops asking for the IDRs it would otherwise request on packet loss, and a lost frame gets concealed instead of repaired. The stream degrades exactly when recovery matters. Now confirmed against the driver: a best-effort `GetVideoParam` with a CodingOption2 buffer chained on, read back for `IntRefType`. Deliberately a SEPARATE query rather than a buffer attached to the existing `BufferSizeInKB` call (which is what the audit finding suggested) — that value backs every bitstream allocation, and a runtime that disliked the chained buffer would take it down with the readback. A failure here costs only the verdict and resolves conservatively (trust the request), so the worst case is the previous behaviour. Verified on Intel UHD 750 (.42), which is where the Intel on-glass run originally caught this: with PUNKTFUNK_INTRA_REFRESH=1, H.264 now logs "silently dropped intra-refresh (GetVideoParam reports IntRefType=0)" and advertises it OFF, while H.265 on the same GPU stays ON — it is genuinely active there. Per-codec, so it could never have been decided statically. 8 live QSV tests pass. Gated on .173: clippy -D warnings at nvenc,amf-qsv,qsv (host + pf-encode --all-targets), amf-qsv without qsv, qsv alone, no-features, cargo test --features qsv, rustfmt — all green. qsv.rs is Windows-only so the Linux legs do not compile it. Closes WP3.2(c), the last Phase 3 item that was hardware-blocked. |
||
|
|
47a23bec12 |
fix(encode/vulkan): unwind every open/import leak, and serve 24-bpp CPU instead of dying on it
decky / build-publish (push) Successful in 23s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
arch / build-publish (push) Successful in 12m42s
windows-host / package (push) Successful in 18m11s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m6s
docker / deploy-docs (push) Successful in 32s
docker / build-push-arm64cross (push) Successful in 6m26s
apple / swift (push) Successful in 6m42s
deb / build-publish-host (push) Failing after 5m39s
android / android (push) Failing after 6m36s
deb / build-publish-client-arm64 (push) Failing after 5m35s
deb / build-publish (push) Successful in 9m40s
ci / rust (push) Successful in 22m26s
ci / web (push) Successful in 52s
ci / docs-site (push) Successful in 55s
ci / bench (push) Successful in 5m54s
ci / rust-arm64 (push) Successful in 11m18s
apple / screenshots (push) Successful in 27m18s
Phase 5's Linux half (audit WP5.1 + WP5.4), each item shaped by the
review that rejected the obvious fix:
Dmabuf import unwind (vk_util): every failure after create_image leaked
the VkImage, and the dup'd dmabuf fd leaked as a raw i32. The sharp edge
is that a SUCCESSFUL vkAllocateMemory transfers fd ownership to Vulkan
(vkFreeMemory closes it), so the naive close-on-error is a double close
that clobbers whatever unrelated descriptor recycled the number. The dup
now lives in an OwnedFd released exactly in the allocate-success arm;
every other path drops it once, and bind/view failures free image+memory.
PyroWave open unwind: open_inner had ~20 fallible steps that each leaked
everything before them (instance, device, pyrowave objects, the whole
CSC pipeline). Rather than a parallel teardown guard — whose reviewed
hazards were a null-unsafe pyrowave_encoder_destroy and a drifting
duplicate of Drop — Self is now constructed right after create_device
with every later resource null, and the existing Drop (wait-idle first,
pw_enc null-guarded, delete-nullptr and VK_NULL_HANDLE destroys are
no-ops) is the single unwind path for error and normal teardown alike.
The ensure_cpu_rgb staging twins (create/allocate/bind, both backends)
and the RGB-direct make_view pair get the same discipline via a shared
make_host_buffer. Observed on hardware: 32 forced import failures, zero
fd drift (the new import_failure_leaks_no_fds smoke on RADV).
24-bpp CPU service (WP5.4): pixel_to_vk had no mapping for the packed
Rgb/Bgr the PipeWire portal negotiates, so a session committed to a path
the backend could not serve and died at its first frame. The filed
open-gate was rejected as a half-mirror — the dmabuf axis is keyed by
fourcc at submit, unknowable at open — so instead the CPU axis is
SERVED: a 3-to-4 expand at the staging upload (normalize_cpu_rgb, the
CPU twin of WP1.4's swscale expand), order-preserving for the CSC
samplers and BGRA-forced for the RGB-direct encode source, whose session
pictureFormat is B8G8R8A8 — the on-glass run caught R-first sources
violating VUID-vkCmdEncodeVideoKHR-pEncodeInfo-08207, a mismatch that
predates this change for plain Rgbx CPU sources. The dmabuf axis feeds
pf-zerocopy's raw-dmabuf degrade latch (
|