be575875723d10b46610c22b55ff97777c19cdec
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6b4be28d24 |
docs(client): write down why the CPU rung is not process-isolated
#97's frame-context floor closes the one rav1d abort we hit and can prove. It does not make the rung panic-proof and nothing at that call site can, because rav1d's public surface is dav1d's C ABI: any reachable panic crosses `extern "C"` as `panic_cannot_unwind` and becomes `abort()`, past every `catch_unwind`, rung demotion and typed refusal we have. Counted across rav1d 1.1.0's 60 source files: 285 `unwrap()`, 214 `assert!`, 19 `unreachable!`, 11 `expect()`, 10 `panic!`. 539 sites that end the client if a stream can reach them. #97 fixed one of them. Process isolation is the only defence that actually works, and this records the decision NOT to build it, with the reasoning, so it is not re-argued from scratch each time someone reads that number: * the defect is upstream's and is one line (memorysafety/rav1d#1497, filed 2026-08-07 with the fix and a reproducer; still open, no PR, as of today); * 539 is an unbounded number, not a risk estimate — none of those sites is known reachable from a punktfunk stream, and the honest next step is to fuzz the rung and find out, which is cheap, rather than buy insurance, which is not; * the cost lands on the video path across Linux, Windows and Android (the Apple clients decode through VideoToolbox and never reach this code), each needing its own shared-memory frame transport, child lifecycle and backpressure, and it adds a scheduling boundary to the slowest rung on the ladder while zero-copy is a hard requirement; * an abort here costs a session that was already degraded — this rung exists because the GPU rungs failed first. The trigger to revisit is named as an event rather than a feeling: a SECOND distinct abort in the field, or a fuzzer finding a reachable panic. Either makes it a class of bugs instead of one, and a class is what would justify the architecture. Documentation only — no behaviour change. |
||
|
|
bca63cd9ff |
docs(client/video): name the upstream rav1d issue next to the workaround
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m13s
apple / swift (pull_request) Successful in 1m30s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 1m53s
ci / bun-nix (pull_request) Successful in 25s
ci / docs-site (pull_request) Successful in 1m23s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m36s
ci / web (pull_request) Successful in 2m22s
android / android (pull_request) Successful in 3m41s
ci / rust (pull_request) Successful in 6m21s
memorysafety/rav1d#1497, filed with the one-line fix and a reproducer that needs no capture — any AV1 stream with one temporal unit removed. Written down where the setting is, because the next person to read `av1_settings` and wonder whether the floor is still needed should be able to check rather than re-derive it. |
||
|
|
32ff93199f |
fix(client/video): a damaged AV1 frame stops killing the whole client
The software rung aborted the process — not the session, the process — the
first time a 4K AV1 stream lost a frame. Reproduced on .21 twice on 08-07,
`SIGABRT` a few hundred milliseconds after "first frame decoded".
It was never about 4K, and it was never our bitstream.
rav1d 1.1.0 kills the process on ANY decode error while it holds a single
frame context. `rav1d_submit_frame`'s `c.fc.len() == 1` branch calls
`rav1d_decode_frame` inline; that always finishes in
`rav1d_decode_frame_exit`, which does an unconditional
`mem::take(&mut f.frame_hdr)` (decode.rs:4873); and then, only if the decode
returned `Err`, the same branch re-enters a local `on_error` whose first act is
`f.frame_hdr.as_ref().unwrap()` (decode.rs:4997) — on the `None` the teardown
just left. The panic unwinds into `dav1d_send_data`, which is `extern "C"`, so
it is `panic_cannot_unwind` → `abort()`: no `catch_unwind` at our call site, no
rung demotion and no `NoSoftwareRung` refusal can catch it. The same code is in
upstream `main` today, and 1.1.0 is the newest release, so there is no version
to bump to.
4K was only where an error first HAPPENED. The CPU rung cannot keep up at
3840x2160 (35-39 fps against a 60 fps stream), so the receive backlog stopped
draining, `pump::data` flushed it and jumped to live, and the next AU
referenced frames nobody had decoded. libdav1d gives the identical verdict on
the identical capture — 13 frames, then "Invalid data found when processing
input" — and simply carries on. At 1080p the rung keeps up, nothing is ever
flushed, no AU is ever damaged, and the same code ran for years without
anybody seeing this.
So the fix is to stop asking rav1d for the configuration whose error path is
broken. `c.fc.len() > 1` never calls `rav1d_decode_frame` at all: it hands the
frame to `rav1d_task_frame_init` and errors come back through `cached_error` /
`task_thread.retval` as ordinary `EINVAL`s, which the pump already answers with
a keyframe request. Measured, against the captured 4K stream:
n_threads=8 max_frame_delay=1 -> n_fc=1 -> ABORT
n_threads=1 max_frame_delay=1 -> n_fc=1 -> ABORT
n_threads=1 max_frame_delay=2 -> n_fc=1 -> ABORT <- proves the rule
n_threads=8 max_frame_delay=2 -> n_fc=2 -> 13 pictures, EINVAL, survives
n_threads=8 max_frame_delay=0 -> n_fc=3 -> survives
The third row is why `n_threads` grows a floor of two as well as the delay:
`n_fc` is `min(max_frame_delay, n_threads)`, so one decode thread silently puts
the whole thing back on the aborting path. That row is also what rules out the
theory this investigation started with — pinning threads to 1 was the suspected
trigger, and it makes things WORSE, so the tile workers are innocent and the
single frame context is the entire defect.
Two frame contexts would normally cost a frame of latency, and this does not,
because `decode` now drains PAST the first `EAGAIN`. `rav1d_get_picture` only
reaches its blocking `drain_picture` on a call whose own `drain` flag is already
set, and that flag is set by the PREVIOUS `get_picture` and cleared by every
`send_data` that carried bytes — so the first `EAGAIN` after a send does not
mean "no picture for this AU", it means "ask again", and this AU's frame comes
out of the second call. Stopping at the first `None` is what a
single-frame-context reading of dav1d's API teaches, and it would have put the
pipeline two frames behind while looking perfectly healthy. Measured over 14
temporal units at `n_fc = 2`: stopping at the first `None` produces nothing at
all for units 0 and 1; draining past it produces one frame per unit from unit 0,
at 20-42 ms per unit against `n_fc = 1`'s 21-53 ms. Not a trade — same cadence,
slightly faster, because the tile workers overlap the drain.
`Av1Software::new` then asks rav1d itself, through `dav1d_get_frame_delay`,
what those settings actually bought, and refuses to open a decoder that would
run with one frame context. That is not a restatement of the arithmetic: it is
`get_num_threads`' own answer, so it stays right if rav1d's derivation changes.
It is there because the failure it guards is uniquely quiet — an edit that
reinstates `n_fc = 1` costs nothing at build time, nothing in the tests and
nothing on a clean link, and then kills the client the first time a frame
arrives damaged. Losing the rung is recoverable; `abort()` is not.
On glass, .21, 35-second sessions, `PUNKTFUNK_DECODER=software`:
4K60 AV1 before: SIGABRT on the second frame, every run
after: exit 0, 0 panics, 35-39 fps, 1204 frames, decode_failed=0,
and 13 decode errors recovered from across 17 backlog
flushes — the exact condition that used to abort, survived
thirteen times in one session
1080p AV1 after: 40 fps, decode p50 2.2 ms (2.1 ms before the change)
What this does NOT buy: rav1d has other `unwrap()`s, and because its whole
public surface is dav1d's `extern "C"` ABI — every internal `rav1d_*` entry
point is `pub(crate)` — no in-process guard can turn one of them into anything
but an abort. This removes the one we hit and can prove; it does not make the
CPU rung panic-proof, and the evidence table says so.
Reported upstream with a self-contained reproducer: the in-tree
`test-25fps.ivf.av1` vector with one temporal unit dropped aborts rav1d at
`n_fc = 1`, survives at `n_fc = 2`, and libdav1d decodes it with 145 error
reports and no crash.
|
||
|
|
5c05246098 |
feat: M10 — FFmpeg is gone from the client
cargo tree -p punktfunk-client-session finds no ffmpeg. The host still does, which is the whole point: pf-encode keeps libavcodec unconditionally and no host workflow, packaging script or licence file was touched. Deleted: crates/pf-ffvk, video_vulkan.rs, video_vaapi.rs, video_libav.rs, the libavcodec half of video_d3d11.rs, the av_log machinery, ffmpeg::codec::Id as the decoder's vocabulary (the quic CODEC_* wire constants now serve, which is why the evidence table was keyed on them), DecodedImage::VkFrame and ::Dmabuf, the presenter's AVVkFrame lane, and the ffmpeg-fallback feature with everything behind it. DrmFrameGuard collapses from an enum to a newtype, which removes an unsafe impl Send. Roughly 25,000 lines. Then the CI, packaging, licensing and docs work the plan's §6 lists: the Windows workflows lose FFMPEG_DIR, PF_FFVK_VULKAN_INCLUDE and their PATH prepend; the MSIX loses its DLL wildcard; the client .deb stops emitting libav sonames on its own because depends come from dpkg-shlibdeps; arch, flatpak and nix drop the dependency; and the README's "FFmpeg 7 or 8" contract narrows to the host. Three defects reached users' machines in the first cut, and none was in the deletion itself. All three desktop Settings UIs offer vulkan, vaapi and d3d11va as stored decoder values, so those strings sit in shipped settings files today. Refusing them by name — which is the correct rule for a stale pin — would have bricked every upgraded client whose owner ever touched that dropdown. They now migrate onto the native rung for the same hardware family, at decoder construction AND at each dialog's lookup, because a legacy value that matches no preset displays as "Automatic" and silently rewrites the user's preference on the next save. M9's evidence filter was deleted on the argument that with no libavcodec twin below, barring an unproven rung removes hardware decode rather than moving down one rung. That is true on Windows and false on Linux for Intel and every unknown vendor id, where prefer_vulkan_first is false and the order is native-vaapi → native-vk: a rung that has decoded nothing anywhere sitting above one that is 250/250 on three drivers. Every Intel Linux desktop would have moved from libavcodec VAAPI, shipping for years, onto pf-vaadec by default — and a rung that constructs and then produces wrong pixels leaves only by the error-streak demotion, which this codebase already documents as not tripping on the B580's strobing. The filter is restored as a narrow, pure, testable rule: an unproven rung yields to a proven one, and to nothing else. Windows deliberately passes no rung below, because that vendor family is the one with a measured wrong-pixel report against Vulkan decode, and trading no evidence for evidence of corruption is the wrong direction. And the notices still said FFmpeg was bundled. The root file is what both desktop clients include_str! and what the MSIX ships, three lines under the new card saying no FFmpeg is bundled; Apple's Acknowledgements said it too, on iOS, tvOS and macOS. The generator now emits four per-client files scoped by transitive closure — 0 FFmpeg mentions in each, verified — while the root file keeps it for the host. That also ends the standing false attribution of ffmpeg-next, GTK4, windows-rs and the NVENC SDK to an iPhone. Windows has no reachable box, so it was compiled instead: a cross clippy at -D warnings on x86_64 and aarch64-pc-windows-msvc with the C toolchain stubbed so build scripts run without linking. That gate immediately caught an include_str! path one directory too deep, which nothing else could have. Gates: container clippy -D warnings, 160 tests, workspace check, both Windows targets clean, client ffmpeg count 0 and host 2. The four decode crates are untouched, so the hardware rungs' 250/250 stands. ⚠ Owed and unrun: no GPU has executed any of this milestone. M8's on-glass software check, M7's D3D11 and VAAPI AV1 hardware legs, and M9's field bake all still want hardware, and the bake window and criteria remain the user's. |
||
|
|
d5e23146c0 |
feat(client): M8 — the software rung is openh264 and rav1d, and swscale is gone
The ladder's last rung no longer runs FFmpeg. H.264 decodes through openh264, AV1 through rav1d, and HEVC is refused outright: no permissively licensed software HEVC decoder exists, so an HEVC session that exhausts its hardware rungs now tears down and re-dials advertising HEVC-less caps, and the host picks H.264. The plan calls that a first-class path; it is one. swscale is deleted, and with it the BT.601 default that its correction code existed to undo. Colour on the H.264 lane now comes from the same pf-bitstream planner every hardware rung submits from — openh264 reports no VUI at all — and AV1's comes per-picture from the sequence header. One colour source, one CSC: the old default is unrepresentable rather than merely fixed. Frames reach the presenter as three tightly-packed planes through the planar CSC pass, which had to be un-gated from the pyrowave feature and its device probe, since the last rung must exist on devices that failed that probe. rav1d rather than the dav1d crate, deliberately and against the plan's literal wording: dav1d-sys is system-deps-only, so it would add a system library and a .pc file to every client package — in the milestone family whose excision checklist exists to delete exactly that. rav1d is the same decoder, same licence, statically linked. The cost is honest: no-asm builds on both decoders, and software throughput is still unmeasured. The colour test is the milestone's exit criterion, so it is built to fail. Three fixtures, and a mutation check: hardcoding the swscale default turns the red bar to [255,24,0], and swapping Cb/Cr turns red to blue — a silent error no metadata assertion could catch. Review then disproved the range half of it numerically: with eight saturated bars, decoding the full-range fixture with the wrong range gives max error ZERO, because a mismatch only pushes values outside [0,1] where the shader clamps. A mid-tone was added; the wrong range now costs 11, well past the tolerance. The exit criterion I set was satisfiable by a test that proved nothing. Two blocking defects, both emergent rather than local. Software AV1 on a 10-bit stream never reached its typed refusal: rav1d is built 8-bit-only and returns ENOPROTOOPT, which the send loop turned into a generic error, so the pump's typed downcast missed and every AU failed identically — a permanent freeze on precisely the shipping case, since AV1 is advertised only where hardware AV1 exists and hardware AV1 plus HDR is Main 10. The shape is now read from the sequence header before any byte reaches the decoder, exactly as the H.264 leg reads the active SPS. And the new Reconnecting phase was the first state that is not streaming, not connecting, and still holding a live stream — which opened all three guards that had made a second launch impossible. Pressing A assigned over `stream` where every other site shuts down first, and StreamState has no Drop, so the old pump was detached: a second live session still submitting to a Vulkan device that gets destroyed underneath it. Nothing about the reconnect was wrong in isolation; the defect lived between a new state and three guards nobody re-examined. Start is now defensive and the retry raises the connecting modal, so the UI matches the state and B can cancel. Also closed: retry_caps was computed, tested and never applied, so a shape refusal could end a session reporting no codec available while a working retry existed; the retry inherited force_software sticky-true, landing an HEVC→H.264 fallback on software H.264 with working hardware H.264; it re-dialled with a stale mode; the CPU present arm had no survivable-failure handling where the pyrowave arm — same pass — has it; HEVC is no longer advertised when the decoder is pinned to software; and the software rung now feeds the recovery-point SEI it already had in hand to the re-anchor gate. ⚠ Two host-side gaps found while tracing, neither in scope here: Hello::launch is NOT idempotent (gog:/custom: targets spawn a second copy on a retry; the field is kept verbatim because dropping it orphans the gamescope display whose reuse key includes the command), and a reconnected session can never adopt a game predating its own launch stamp, so it has no game-exit detection. ⚠ OWED: the on-glass software run. ~200 lines of new Vulkan on a path that only runs because the GPU already failed, and no driver has seen it. The review's minimum check is sync validation enabled, a non-multiple-of-16 mode, a mid-session resize and demotion, and both colour matrices. Gates: container clippy -D warnings over four crates, 236 tests, workspace check. pf-vkdecode and pf-bitstream are byte-for-byte untouched, so the hardware rungs' 250/250 stands. |
||
|
|
caa47e28e6 |
fix(client/decode): AV1 hardware decode stops silently opening libdav1d
apple / swift (push) Successful in 1m27s
ci / rust (push) Successful in 5m28s
android / android (push) Successful in 5m59s
ci / rust-arm64 (push) Successful in 2m24s
ci / web (push) Successful in 1m55s
arch / build-publish (push) Successful in 9m23s
ci / docs-site (push) Successful in 1m45s
deb / build-publish-client-arm64 (push) Successful in 2m23s
windows-host / package (push) Successful in 12m12s
windows-host / winget-source (push) Skipped
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 37s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 9s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
release / apple (push) Successful in 12m6s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 7s
deb / build-publish (push) Successful in 5m54s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
deb / build-publish-host (push) Successful in 5m19s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 1m0s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m43s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 27s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m44s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 7s
windows-host / canary-manifest (push) Successful in 24s
docker / deploy-docs (push) Successful in 45s
docker / builders-arm64cross (push) Successful in 8s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m7s
apple / screenshots (push) Successful in 5m51s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m17s
flatpak / build-publish (push) Successful in 5m38s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m7s
avcodec_find_decoder(id) returns the registry's FIRST decoder for the id, and
upstream orders the native av1 decoder LAST on purpose ("hwaccel hooks only,
so prefer external decoders" — allcodecs.c). All three hardware backends
selected by id, so every AV1 session opened libdav1d: a software decoder that
silently ignores hw_device_ctx and never calls get_format. Each frame then
failed the backend's hw-format guard and the session burned the demotion
ladder MID-STREAM — field-logged as 68 Vulkan fails → D3D11VA → 102 fails →
software, ~3 s of black — with "hardware decode active" already printed and
the D3D11 profile/pool probes all green. H.264/HEVC never hit this only
because their native decoders happen to be registered first.
Selection is now by capability: find_hw_decoder walks av_codec_iterate and
takes the first decoder whose avcodec_get_hw_config advertises the backend's
surface via HW_DEVICE_CTX, so a build without a usable hw decoder fails at
OPEN in milliseconds and the ladder runs there — the idiom the D3D11 probes
already follow. Registry order still wins among capable decoders, so
H.264/HEVC select exactly what they always did. The software path keeps the
id lookup on purpose: libdav1d is the fastest CPU AV1, and the native av1
decoder has no software path at all.
Every decode log now carries the selected decoder's name — decoder="av1" vs
decoder="libdav1d" is the whole diagnosis, and no log line said it. The
session log names the WIRE codec and drops the FFmpeg id for PyroWave
(ffmpeg_codec_id's fallthrough claimed codec_id=HEVC for wavelet sessions
that never touch FFmpeg).
The CPU lane also stops passing raw PQ off as a tone-map: software-decoded
frames deliberately never take the HDR10 swapchain, but a PQ stream there was
then shown UNtonemapped (washed out) with no warning — the pq-downgrade warn
keys off the swapchain answer — while the Detailed OSD badge claimed the
"HDR→SDR" tone-map that only the hardware lane's CSC runs. The presenter now
warns once when a PQ CpuFrame arrives, and the badge distinguishes
"HDR→SDR (raw)" (no tone-map pass) from the hardware lane's real "HDR→SDR".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
e1ddd49e37 |
fix(client-core,ffvk): close the proof-lint hole in two of the three unguarded crates
`clippy::undocumented_unsafe_blocks` is what makes the SAFETY convention a rule rather than a habit,
and three crates had never adopted it — pf-client-core (91 unsafe items), pf-presenter (123) and
punktfunk-core (167) — while every other subsystem crate denied it. That gap is why the decoders'
`unsafe impl Send`s carried a one-line aside instead of an argument: nothing required one.
pf-client-core and pf-ffvk now deny it, with a proof written for all 58 + 3 sites they had.
⚠️ 44 of those 58 were WINDOWS-ONLY — `clipboard.rs` 24 and `video_d3d11.rs` 20 — and invisible to
the Linux measurement that sized this work at 14. Same trap as the E0133 sweep: a Linux-only survey
of a cross-platform crate undercounts by whatever the `cfg` hides, here by 3x. Landing the deny on
the strength of that number alone would have re-broken Windows CI, which is exactly the mistake this
session already made once with the `warn`-that-was-really-`deny`.
The proofs say what is actually load-bearing rather than restating the call. In `clipboard.rs` that
is the ownership split Win32 requires and nothing in the code stated: `GetClipboardData` returns a
handle BORROWED from the clipboard (never freed here), while `GlobalAlloc` + `SetClipboardData`
TRANSFERS ownership to it (which is why nothing frees that one either) — two opposite rules, three
lines apart. In `video_d3d11.rs` the recurring one is that libav's `get_format` list is
NUL-terminated by `AV_PIX_FMT_NONE`, which is what keeps the walk in bounds.
Remaining: punktfunk-core (~146, of which `abi.rs` is 141) and pf-presenter (~108). Both want the
"state the contract once" treatment — abi.rs's sites are a handful of repeating shapes (`opt_cstr`
on caller C strings, null-guarded out-param writes, forwarding calls), not 141 distinct arguments.
Note the vendored `fec-rs` (18 sites) is a separate path-dependency crate, so it is out of scope
rather than something to prove.
Verified: Linux .21 fmt + both CI clippy steps rc=0; Windows .47 `-p pf-client-core` clippy
`-D warnings` rc=0 (the only place the 44 are visible), plus the full Windows CI clippy set and
pf-capture's 18 tests.
|
||
|
|
570ff504ad |
refactor(client-core/W8): split video.rs into flat decoder-backend siblings
Break the 1974-line pf-client-core/src/video.rs into flat sibling modules (matching the crate's video_d3d11.rs / video_pyrowave.rs convention), leaving video.rs as the contract + Decoder dispatch facade: - video_color.rs : ColorDesc + csc_rows (the Y'CbCr->RGB matrix) - video_software.rs : the libavcodec/swscale SoftwareDecoder - video_vaapi.rs : the Linux-only VAAPI/DRM-PRIME backend (mod is cfg(linux)) - video_vulkan.rs : the FFmpeg Vulkan Video backend Every crate::video::X / video::X path stays byte-stable (ColorDesc + csc_rows re-exported from video.rs; frame POD, VulkanDecodeDevice, QueueLock, Decoder, decodable_codecs*, ffmpeg_codec_id, fourcc/drm_fourcc_for all stay in video.rs). Code-driven placements: averr, AVERROR_EAGAIN, frame_is_keyframe stay in video.rs (shared by all three decoders); DrmFrameGuard's field + drm_fourcc_for + Software/Vaapi/VulkanDecoder ctors/decode became pub(crate) (sibling access); the test module split three ways (software tests need private decoder internals). Pure move; no behavior change. Verified on Linux (home-worker-5): cargo clippy -p pf-client-core (default [pyrowave] + --no-default-features, --all-targets -D warnings) + cargo test. Windows verify BLOCKED environmentally: pf-client-core -> sdl3 build-from-source -> CMake/CL.exe fails on winbox's non-ASCII home path (fails the baseline too, independent of this split); the split's Windows surface (facade cfg(windows) bits + video_d3d11) is verbatim-preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |