Commit Graph
3 Commits
Author SHA1 Message Date
enricobuehlerandClaude Fable 5 9b3ec9204c fix(capture): a refused EGL→CUDA dmabuf offer stops being asked
ci / rust (push) Failing after 41s
android / android (push) Canceled after 1m19s
apple / screenshots (push) Canceled after 0s
apple / swift (push) Canceled after 1m20s
arch / build-publish (push) Canceled after 1m23s
ci / rust-arm64 (push) Canceled after 1m24s
ci / web (push) Canceled after 1m24s
ci / docs-site (push) Canceled after 1m23s
ci / bench (push) Canceled after 1m22s
deb / build-publish-client-arm64 (push) Canceled after 0s
deb / build-publish (push) Canceled after 46s
deb / build-publish-host (push) Canceled after 2s
decky / build-publish (push) Canceled after 1s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Canceled after 0s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Canceled after 0s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 0s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 0s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Canceled after 0s
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
windows-host / winget-source (push) Canceled after 0s
windows-host / package (push) Canceled after 1m35s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 7s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 5s
The raw-dmabuf passthrough has had a negotiation-timeout latch since the
hybrid-Intel case: one conclusive timeout and later captures negotiate the
CPU path instead of re-paying it. The EGL→CUDA dmabuf-only offer had no
equivalent — a compositor that accepts none of the importer's modifiers
timed out the same 10 s negotiation on every session, forever, under the
generic 'format negotiation never completed' diagnosis.

Now the symmetric latch (note_gpu_dmabuf_negotiation_failed) gates
build_importer in the one negotiation resolver, scoped to this offer alone
(raw passthrough, worker-death latch, encoder untouched), with the same
operator escape as the raw arm: an explicit PUNKTFUNK_ZEROCOPY=1 keeps
erroring loudly instead of downgrading.

One correctness detail beyond the handoff's sketch: whether the GPU offer
was ACTUALLY advertised is a runtime fact of the PipeWire thread (the
importer may fail to construct — no CUDA — in which case no dmabuf was
offered and a timeout must not latch it off). plan.build_importer alone
cannot answer that, so the thread records the made-offer on CaptureSignals
(gpu_dmabuf_offer) and the timeout diagnosis branches on the offer that
really happened, not the plan's intent.

Also renames vaapi_dmabuf_forced → zerocopy_forced (it now guards both
timeout arms; single caller). New plan invariant pinned in tests: the
latch gates only the importer, never the raw passthrough.

(V3 from design/pf-zerocopy-sweep-handoff.md — the deferred design call,
now decided in favour of the symmetric latch.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 13:15:36 +02:00
enricobuehlerandClaude Fable 5 143a707f76 fix(zerocopy): the mechanical sweep batch — truthful fence waits, forgiving env flags, no leaked planes
From the pf-zerocopy review sweep (design/pf-zerocopy-sweep-handoff.md), the
compile-verifiable batch:

- dmabuf_fence: the blocking poll's result was discarded — EINTR silently
  skipped the wait (reopening the stale-frame race a SIGCHLD away) and a
  timeout reported as waited. Now EINTR retries with the remaining budget
  and the caller gets Signaled/TimedOut/NoFence, so the one diagnostic
  operators have about implicit fencing stops lying. (C2)
- env flags: PUNKTFUNK_ZEROCOPY=TRUE meant *off* — values are case-folded
  now, and an unrecognised spelling falls back to the flag's default with a
  one-shot warning instead of silently inverting the operator's intent. (C3)
- cuda: alloc_pitched_nv12 leaked the Y plane when the UV allocation failed
  (per-frame under VRAM pressure — the worst possible time to leak); a failed
  async-copy enqueue now drains the stream before returning, so a recycled
  pool buffer can't race an orphaned in-flight copy. (C4, C5)
- worker: --fd is validated (>= 3, fstat + S_ISSOCK) before OwnedFd adoption
  — 'zerocopy-worker --fd -1' was constructing OwnedFd's niche value. (V1)
- docs: all 15 rustdoc warnings fixed, including the link to the renamed
  note_raw_dmabuf_negotiation_failed. (D1)
- CI: a SPIR-V drift gate — the committed .spv blobs are include_bytes!'d and
  rebuilt by hand; the gate diffs disassembly (filtering only the
  shaderc/glslang generator difference) so a forgotten rebuild fails CI
  instead of shipping the old kernel. Both blobs verified in sync. (S1)
- tests: bt709_limited pinned to external BT.709 anchors (it is the sole
  oracle for the GPU colour self-test); blend_geometry gets its first tests —
  empty rect, CURSOR_MAX clamp, per-format group counts, and negative-ox
  floor alignment. (T2, T3)

Verified on .25: clippy -D warnings clean, 27/27 tests, cargo doc 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 12:40:37 +02:00
enricobuehlerandClaude Opus 5 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>
2026-07-26 11:25:39 +02:00