3b11288c97062411e6098eb529791787a2657f2c
21
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
e92a0aaa00 |
fix(zerocopy/vkslot): a timed-out blend can't corrupt its slot, and NV12 cursor chroma sits on the grid
Two [GPU]-class defects from design/pf-zerocopy-sweep-handoff.md; the code is compile- and unit-verified, the visual halves still owe on-glass time. - blend_ref reused a fence and command buffer after a wait timeout: on TIMEOUT it reset a fence that still had a pending signal and re-recorded a command buffer the GPU might still be executing — reached exactly in the contended case the CPU-synced path exists for (no timeline export + visible cursor + a >1 s GPU stall). A failed wait now drains the device before the reset (the VkBridge::import_linear precedent), and free_slots quiesces unconditionally instead of only when a timeline exists, so teardown after a timed-out sync blend no longer frees objects an outstanding submission references. (C1) - cursor_blend.comp anchored NV12 chroma blocks to the cursor's oy, not the surface chroma grid: for odd oy every UV sample averaged luma rows one below the rows it covers (a one-row colour fringe on the cursor's edges, ~half of all cursor positions) and the cursor's last row's chroma was never written. Block rows now anchor to the chroma grid the same way spans anchor to the word grid — y0 = floor(oy/2)*2, per-row cy guards for the straddle block — and blend_geometry counts blocks from the same anchor (one extra block when oy is odd; covered by new tests, including negative oy). cursor_blend.spv rebuilt (glslangValidator, spirv-val clean, drift gate passes). (C6) Owed on-glass: a stalled-GPU cursor session for C1; an odd-oy cursor colour check for C6 (subtle fringe on the cursor's top/bottom edge). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fa083f50d3 |
fix(zerocopy): unsafe fn bodies join the unsafe-proof program
#![deny(clippy::undocumented_unsafe_blocks)] never inspected the body of an unsafe fn — operations there are not "unsafe blocks" — so roughly 20 functions' worth of raw GL/CUDA/Vulkan driver calls sat outside the invariant the crate advertises. Concretely, that blind spot is why the constructor-leak and teardown-ordering shapes fixed earlier in this series could ship without ever prompting a reviewer. #![deny(unsafe_op_in_unsafe_fn)] now closes the gap: every unsafe fn body is an explicit unsafe block carrying a SAFETY comment that names the caller contract it relies on (the dlopen'd CUDA wrapper table gets a uniform forward-to-live-table proof). Mechanical; no behavior change. (V2 from design/pf-zerocopy-sweep-handoff.md.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c677732c60 |
fix(zerocopy/client): a wedged worker can't park the reaper, and no worker outlives the host
Three related reaper problems (R5 from design/pf-zerocopy-sweep-handoff.md): - sweep_reaper called kill() + blocking wait() while holding the global REAPER mutex. A worker wedged in a driver ioctl is in D state and ignores SIGKILL, so wait() never returned — parking every later spawn() and every importer drop() behind the lock. Expired entries are now drained under the lock and killed outside it, with a bounded (~100 ms) try_wait poll; a worker that still won't die is parked again for a later sweep (re-killing is harmless) instead of blocking anyone. - No PR_SET_PDEATHSIG: if the host died, the worker survived holding its CUcontext + BufferPool — by the code's own comment, hundreds of MB of VRAM. The pre_exec closure (async-signal-safe: prctl/getppid/dup2/fcntl only, no allocation) now arms SIGKILL-on-parent-death with the standard getppid race guard. Not taken: arming the 20 s kill deadline from a timer instead of the next spawn/drop (the handoff's optional third leg). PDEATHSIG closes the worst-case orphan; a wedged worker after the LAST capture of a session still waits for the next spawn to be swept. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3c62da3b8e |
fix(zerocopy/client): renegotiation retires the old generation's IPC mappings
Shared::mappings only ever grew: clear_cache reset sent_keys and told the worker to drop its fd cache, but nothing closed the host-side CUDA IPC mappings for the previous pool generation. A session that renegotiates repeatedly (mode changes, HDR toggles, client reconnects) accumulated a pool's worth of stale mappings each time, each pinning a host VA reservation to peer memory the worker had already freed. Mappings now carry a refcount and a retired flag. clear_cache closes every unreferenced mapping immediately and marks the rest retired; a retired mapping closes when its last in-flight DeviceBuffer releases. The worker's half of the contract: ClearCache also forgets its VA→id map, so anything delivered after the boundary gets a fresh id WITH its descriptor — without that, a same-shape renegotiation (worker keeps its pool) would re-deliver an old id whose host mapping was just closed, and the host would misread it as a desync. Ids never repeat (next_id only counts up), so fresh ids cannot collide with the graveyard. (R6 from design/pf-zerocopy-sweep-handoff.md.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a2033d6c82 |
fix(zerocopy/vulkan): VkBridge bring-up and the CSC build unwind instead of leaking
VkBridge::new leaked its instance (and past device creation, the device and command pool too) on every error path — reached repeatedly, because a box whose Vulkan device refuses the external-memory extensions retries the bridge on every LINEAR frame. Pre-device failures now destroy the instance explicitly (the VkSlotBlend::new shape); after device creation the remaining objects build into an incrementally-filled struct whose existing Drop tolerates the nulls a partial init leaves (Vulkan destroy calls are defined no-ops on VK_NULL_HANDLE). ensure_csc had the same hole across its six-object pipeline build; the fallible half now fills the Csc front to back and a mid-build failure destroys exactly what was created, leaving self.csc None for a clean retry. (R3 from design/pf-zerocopy-sweep-handoff.md.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8d02255703 |
fix(zerocopy/egl): construction can fail late, so teardown must be structural
Three unwind holes in the EGL side, all of the same shape — fallible construction over raw handles with no Drop to unwind through — and all retried per frame, so a sustained failure (VRAM pressure is the realistic one) leaked unboundedly: - The GlBlit/Nv12Blit/Yuv444Blit constructors interleave GL-object creation with fallible steps (FBO completeness, CUDA registration, pool allocation). A GlNameGuard now owns every bare GL name until the final struct exists; on unwind it deletes them AFTER the RegisteredTexture locals unregister (declaration order), preserving the unregister-before-delete invariant. gl.rs gets the same treatment for its compile paths: the vertex shader on a fragment-compile failure, the program on a link failure. (R1) - EglImporter::new leaked the DRM render-node fd and the whole gbm_device on every '?' after their creation (most realistically cuda::context() failing on a host where EGL comes up but CUDA does not). Both now live in a GbmDevice with its own Drop: destroy the device, then close the fd. (R2) - EglImporter's manual Drop destroyed the gbm device and closed the fd BEFORE field drops ran the blit destructors, which then called cuGraphicsUnregisterResource/glDeleteTextures against a dead native display — the stale-driver-state class this path once crashed on. The Drop impl is gone; teardown is now purely field order (blits and bridge first, GbmDevice last), and the blit SAFETY comments cite that order instead of the previously-false claim. (R4) (R1/R2/R4 from design/pf-zerocopy-sweep-handoff.md.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8de5ba4092 |
test(zerocopy): the fd must actually cross the socket, not just claim to
SCM_RIGHTS descriptor passing is the mechanism the whole worker isolation design rests on, and no test verified it end to end: both suites asserted only the has_fd boolean parsed from the JSON body. Setting the send-site's descriptor to None — zero-copy broken in production — stayed green; so did dropping the received fd in the worker's dispatch loop. Now the client-side scripted peer records the st_ino of every descriptor that arrives and the tests assert the sequence against dmabuf_key() of the sent plane (SCM_RIGHTS re-numbers the fd but preserves the open file description, so the inode is the identity the worker keys its cache on); the worker dispatch test sends one import with a live fd and asserts the backend received a descriptor with the sender's identity. (T1 from design/pf-zerocopy-sweep-handoff.md) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
dff63b2a29 |
fix(encode/nvenc): a visible cursor no longer serializes submit — the blend goes stream-ordered
windows-host / winget-source (push) Canceled after 0s
windows-host / package (push) Canceled after 2m32s
android / android (push) Canceled after 39s
apple / swift (push) Canceled after 0s
apple / screenshots (push) Canceled after 0s
arch / build-publish (push) Canceled after 34s
ci / rust (push) Canceled after 24s
ci / rust-arm64 (push) Canceled after 24s
ci / bench (push) Canceled after 5s
ci / web (push) Canceled after 24s
ci / docs-site (push) Canceled after 8s
deb / build-publish-client-arm64 (push) Canceled after 0s
deb / build-publish (push) Canceled after 2s
deb / build-publish-host (push) Canceled after 1s
decky / build-publish (push) Canceled after 4s
docker / build-push-arm64cross (push) Canceled after 0s
docker / deploy-docs (push) Canceled after 0s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Canceled after 5s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Canceled after 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Canceled after 5s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Canceled after 2s
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
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
Report: iPad on a gamescope/NVIDIA 120 fps session capped at ~80 fps with repeat_fps 0, zero loss, capture 0 µs, ASIC 15 µs — and submit p50 at 10.2 ms, ~81 % of the loop period. Under gamescope the host composites the live pointer into EVERY frame, and a cursor-bearing frame forced the CPU-synced submit path: a blocking CUDA copy plus a fence-waited Vulkan blend, both exposed to the running game's GPU load. The "games hide the cursor" assumption the gate relied on does not hold under gamescope. The blend is now stream-ordered end to end. VkSlotBlend exports a timeline semaphore (VK_KHR_timeline_semaphore + external_semaphore_fd) into CUDA (cuImportExternalSemaphore, new dlopen entries): the enqueued copy signals it on the encode thread's copy stream, the blend submission waits for and advances it on the Vulkan queue, and a CUDA-side wait orders the encode after the blend on the session's bound IO stream — no CPU sync anywhere, so cursor frames keep the stream-ordered fast path. Each ring slot gets its own command buffer + descriptor set (written once) so several ordered blends can be in flight; cursor-bitmap uploads and teardown quiesce through the timeline. Drivers without the timeline export keep the previous CPU-synced blend, and any bring-up or per-frame failure still degrades to "no cursor", never a dropped frame. Also: the blocking multi-plane copies (the escalated/pipelined mode and the non-stream-ordered fallback) now enqueue every plane and pay ONE stream sync instead of one per plane (NV12 2→1, YUV444 3→1) — each exposed wait costs scheduling latency under GPU contention, which is what makes the escalation's blocking copies self-reinforcing. Verified on the RTX 5070 Ti box (driver 610.43.03): all 12 nvenc_cuda on-hardware smokes green, including the new nvenc_cuda_cursor_blend_stream_ordered (6 cursor AUs, all ordered, across a bitmap-serial flip); host suite 301/301; clippy --all-targets -D warnings clean; struct layouts of the hand-flattened cuda.h params asserted in tests. 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
|
||
|
|
3efbe4164e |
fix(capture): capture CPU frames once the encoder proves it can't import dmabufs
audit / cargo-audit (push) Successful in 2m13s
audit / bun-audit (push) Failing after 13s
ci / web (push) Successful in 57s
ci / docs-site (push) Successful in 1m3s
windows-host / package (push) Successful in 10m18s
ci / bench (push) Successful in 6m41s
android / android (push) Successful in 12m26s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 14s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 13s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
arch / build-publish (push) Successful in 12m30s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m10s
ci / rust-arm64 (push) Successful in 11m46s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m17s
deb / build-publish (push) Successful in 11m17s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m13s
deb / build-publish-host (push) Successful in 12m35s
deb / build-publish-client-arm64 (push) Successful in 8m23s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 2m5s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 7m40s
apple / swift (push) Successful in 5m28s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8m6s
docker / deploy-docs (push) Successful in 14s
ci / rust (push) Successful in 21m44s
docker / build-push-arm64cross (push) Successful in 7m18s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m37s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m14s
flatpak / build-publish (push) Successful in 6m38s
release / apple (push) Successful in 29m56s
apple / screenshots (push) Successful in 25m36s
A dmabuf import the GPU driver refuses is refused identically on every retry, but the only recovery above it was the encode-stall ladder: five in-place encoder rebuilds, then the video session ends. So a host whose driver will not take what its compositor allocates lost every session on its first frame, and every reconnect repeated it — while the very same host streamed fine with `PUNKTFUNK_ZEROCOPY=0`. The software knew how to run that machine and never chose to. Latch it, exactly as the sibling CUDA-import path already does after repeated worker deaths: three consecutive import failures with no frame in between disable the raw-dmabuf passthrough for the host process, and capture negotiates CPU frames from the next session on. Three sits below the encoder's rebuild budget, so the latch is set before the session it doomed ends — one bad session, then a working (if slower) host, with a log line saying which and why instead of an operator having to find an environment variable. Only the two stages that ARE the import are counted — the buffersrc push of the DRM-PRIME descriptor and the buffersink pull where `hwmap` maps it into a VA surface. `avcodec_send_frame` is deliberately left out: that one is the encoder stalling, which the in-place rebuild exists to recover, and taking zero-copy away permanently over a transient fault would be a bad trade. The latch lives in pf-zerocopy because it is the leaf both sides can see — the capture→encode edge is one-way by design, so pf-capture cannot ask pf-encode anything. |
||
|
|
7c82a72ecd |
fix(zerocopy): find the NVIDIA render node instead of assuming renderD128
The EGL importer — the head of the CUDA/NVENC zero-copy path — opened
`/dev/dri/renderD128` and hoped. On a single-GPU host that is the
NVIDIA node and it always worked. On a hybrid laptop the iGPU is bound
first, so renderD128 is Intel: we built a GBM device on Mesa, asked it
for a pbuffer-capable OpenGL config, got none (Mesa's GBM platform
advertises only window-capable ones — gbm_surface is the point of that
platform there), and reported
zerocopy worker init failed: no EGL config for OpenGL
on a machine whose NVIDIA EGL stack was demonstrably fine, naming
neither the device nor the reason. `PUNKTFUNK_RENDER_NODE` was no
escape either: this path never read it.
Pick the node by what it is — the first `/dev/dri/renderD*` whose
sysfs PCI vendor is 0x10de, the same identification the crate's Vulkan
bridge already uses for its physical device. A local scan rather than a
`pf_gpu` dependency, because this crate is a leaf whose worker is its
own process, and `pf_gpu::linux_render_node` answers a different
question anyway: it follows the operator's GPU *preference*, which may
legitimately name the iGPU. What is needed here is not which GPU to use
but where CUDA is. `PUNKTFUNK_ZEROCOPY_RENDER_NODE` overrides, and no
NVIDIA node found keeps the historical guess — sysfs may just not be
mounted, and the CUDA context is what properly fails on a host with no
NVIDIA at all.
Then stop the config query being able to fail this way: retry without
the surface-type constraint, which is honest because we never create an
EGLSurface — `eglMakeCurrent` runs surfaceless. And name the node in
every error and in the ready line, so the next hybrid host diagnoses
itself.
|
||
|
|
d2c46eaf3c |
feat(encode/nvenc): SPIR-V cursor blend over Vulkan-allocated input slots — retire the PTX kernels
A vendored PTX blob is JIT'd against the driver's ISA ceiling, so the cursor-blend module silently dies on drivers older than the generating toolkit (CUDA_ERROR_UNSUPPORTED_PTX_VERSION/INVALID_PTX, 222/218 — the KWin leg's invisible composite cursor on driver 595/CUDA 13.2 vs a CUDA 13.3 blob). SPIR-V has no such coupling, and the in-tree precedent already exists twice (vulkan_video's CSC blend, VkBridge's exportable OPAQUE_FD → cuImportExternalMemory bridge). New pf_zerocopy::vkslot::VkSlotBlend: the direct-SDK NVENC encoder now allocates its input ring as exportable Vulkan buffers CUDA-imports (same contiguous InputSurface layouts, pitch = row bytes rounded to 256), and the cursor composite is a compute dispatch over the cursor's rectangle (cursor_blend.comp, vendored .spv; spec-constant selects ARGB/NV12/YUV444; BT.709 limited, matching the retired .cu). The surface SSBO is uint[] with every invocation owning whole words — no 8-bit-storage device dependency. Cursor-bearing frames force the existing CPU-synced submit path so the CUDA copy → Vulkan dispatch (fence-waited) → NVENC encode ordering is CPU-established; cursorless frames keep the stream-ordered fast path untouched. Any bring-up/alloc/registration failure falls back wholesale to plain pitched CUDA surfaces (never a mixed or short ring): sessions always encode, composite mode just loses the cursor, warned once. cursor_blend.cu / cursor_blend.ptx and the CursorBlend PTX loader are deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1262fec2ae |
perf(zerocopy/vulkan): LN4 — global-priority VkBridge queue for the LINEAR/gamescope CSC
apple / swift (push) Successful in 1m22s
apple / screenshots (push) Successful in 6m27s
ci / web (push) Successful in 1m0s
ci / docs-site (push) Successful in 1m6s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
ci / bench (push) Successful in 7m51s
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 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
deb / build-publish (push) Successful in 8m47s
arch / build-publish (push) Successful in 15m47s
android / android (push) Successful in 16m2s
deb / build-publish-host (push) Successful in 12m13s
ci / rust (push) Successful in 26m31s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 15m54s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m3s
docker / deploy-docs (push) Successful in 16s
The RGB→NV12 compute CSC shares the SMs with the game; request an
elevated global-priority queue (VK_KHR/EXT_global_priority) so the
driver can schedule it ahead. PUNKTFUNK_VK_QUEUE_PRIORITY =
off|high|realtime (default realtime), downgrading REALTIME→HIGH→none
on NOT_PERMITTED/INITIALIZATION_FAILED — a refused class never fails
the bridge. Mirrors PyroWave's
|
||
|
|
cd36c46388 |
perf(encode/nvenc-linux): LN2 v1 — stream-ordered submit via NvEncSetIOCudaStreams
Bind the session's IO streams to the encode thread's high-priority copy stream: in sync-retrieve depth-1 use the per-frame input copy and cursor blend now enqueue with NO cuStreamSynchronize and encode_picture orders after them on the stream. Same stream both directions, so the encode's completion is inserted into the stream and later work (the next frame's copy into a reused ring slot) waits for it. Soundness gate: the fast path engages only when pending is empty (true depth-1 usage) — every prior encode was drained by a blocking poll, and the caller holds the frame payload across the matching poll (contract now documented on Encoder::submit; both host loops already comply). Pipelined callers and PUNKTFUNK_NVENC_ASYNC mode keep the blocking copies. True zero-copy input registration (registering the worker-owned IPC buffer directly) stays the LN2 v2 follow-up — it needs a contiguous worker-pool NV12 layout and a registration<->IPC-mapping lifetime tie. PUNKTFUNK_NVENC_STREAM_ORDERED=0 restores the old blocking behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
26ff005e70 |
fix(zerocopy): error-path leaks, exception-safe fence wait, odd-height NV12 overrun, worker reaper deadline
Seven medium findings from the round-1 sweep, all in pf-zerocopy. Adjudicated against source; all zero-copy-preserving (no GPU→CPU→GPU roundtrip introduced). - import_src (vulkan.rs) leaked a VkBuffer + VkDeviceMemory + dup'd fd on every fallible step before the success-only src_cache.insert. A failed import is survived by the worker and RETRIED by the caller every frame, so this leaked per frame for the worker's whole lifetime. Now owns the dup fd via OwnedFd (closes on early return until Vulkan consumes it at allocate_memory) and destroys the buffer + frees memory on each error path. - ensure_dst destroyed the old exportable buffer and nulled self.dst BEFORE building the replacement, so a failed rebuild both dropped the working buffer and leaked every object the partial rebuild created (raw ash handles, no Drop; VkBridge::drop only frees the live self.dst). Now builds fully, unwinds each error locally, and swaps only on success. - The submit→wait→reset sequence in import_linear_nv12 / import_linear `?`-ed out of wait_for_fences BEFORE reset_fences on a TIMEOUT/DEVICE_LOST, leaving the shared self.cmd PENDING and self.fence IN-USE while the caller retries on the same bridge (and ensure_dst later destroys dst.buffer assuming nothing is in flight). Now drains the GPU (device_wait_idle) and resets the fence before propagating — fixing the reported ensure_dst UAF at its source. - copy_pitched_nv12_to_buffer writes height.div_ceil(2) chroma rows into a UV plane sized at height/2, so an odd height overruns by one uv_pitch row (OOB device write / CUDA_ERROR_ILLEGAL_ADDRESS). Added the even-dimension guard to import_linear_nv12, matching Nv12Blit/Yuv444Blit. - sweep_reaper only reaped already-exited workers, so a worker wedged inside a driver call lingered forever holding its CUcontext + BufferPool (hundreds of MB VRAM). Now force-kills a child parked past REAPER_KILL_DEADLINE (20s). Compile + tests green on Linux .21 (RTX 5070 Ti): pf-zerocopy 17/0. The error paths themselves are not fault-injected; the fixes are structural. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
986402f731 |
fix(inject,zerocopy,capture): teardown deadlock, GL→CUDA copy race, cursor-meta OOB read
The three high-severity defects from the round-1 sweep of pf-inject / pf-zerocopy / pf-capture (adjudicated against source — all 7 reported criticals in these crates downgraded; these were the real highs). - pf-inject steam_gadget: `SteamDeckGadget::drop` set `running=false` then joined the control thread, which spends steady state parked in a blocking, no-timeout `EVENT_FETCH` ioctl that only tests `running` at its loop top. The flag never reaches it, closing the fd can't wake an in-flight ioctl (the syscall holds a file reference, and the fd is shared via Arc by the very threads being joined), so the join hung — and it runs on the session input thread via `PadSlots::sweep`, driven by the client's `active_mask`, so a remote peer clearing its pad bit could freeze all session input. Now wakes the parked threads with SIGUSR1 (no-op, non-SA_RESTART handler → the ioctl returns EINTR and the loop exits), retried until each reports done and bounded (~1s). - pf-zerocopy cuda: the GL→CUDA "sync point" was never established for the copy. `cuGraphicsMapResources`/`UnmapResources` were issued on the NULL stream, but the D2D copy runs on `copy_stream()`, a `CU_STREAM_NON_BLOCKING` stream exempt from implicit NULL-stream ordering — and the GL de-tile/CSC that produced the texture ends with only `glFlush` (no fence). So the copy could race ahead of the not-yet-retired GL draw: intermittent stale/torn frames under GPU load, on the default NVIDIA capture→encode path. Map, copy, and unmap now share `copy_stream()`, so map's device-side guarantee orders the GL work before the copy. Zero-copy preserved (no GPU→CPU→GPU roundtrip). - pf-capture cursor meta: `update_cursor_meta` trusted three producer-written fields (bitmap_offset, pixel offset, stride) with no bound against the metadata region, driving OOB pointer arithmetic and an oversized `from_raw_parts` — an OOB read that SIGSEGVs inside the PipeWire `.process` callback (uncatchable by the surrounding `catch_unwind`). Switched to `spa_buffer_find_meta` to obtain the region's real `size` and validate every offset with checked arithmetic before each deref/slice, mirroring the fd-length guard the main frame path already applies. Compile + existing tests green on Linux .21 (real RTX 5070 Ti): pf-inject 74/0, pf-zerocopy 17/0, pf-capture 1/0. The gadget deadlock path only executes on a SteamOS host with raw_gadget/dummy_hcd (not reproducible on the CachyOS box), so that fix is reasoned + compile-verified, not runtime-exercised. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
5e1e64e50b |
perf(latency): T2.5b — NV12 compute CSC on the LINEAR/gamescope zero-copy path
apple / swift (push) Successful in 1m21s
apple / screenshots (push) Successful in 6m23s
ci / web (push) Successful in 54s
arch / build-publish (push) Successful in 10m52s
ci / docs-site (push) Successful in 1m7s
decky / build-publish (push) Successful in 35s
android / android (push) Successful in 13m2s
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 16s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
ci / bench (push) Successful in 6m8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7m56s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9m44s
docker / deploy-docs (push) Successful in 24s
deb / build-publish (push) Successful in 11m52s
ci / rust (push) Successful in 27m4s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m26s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m20s
design/latency-reduction-2026-07.md T2.5's Linux half: the LINEAR dmabuf path (gamescope's only offer) fed NVENC RGB, paying its internal RGB->YUV CSC on the SM the game is saturating — the exact contention §5.A removed everywhere else. The Vulkan bridge now carries a buffer-to-buffer RGB->NV12 compute shader (rgb2nv12_buf.comp, BT.709 limited, coefficient-identical to pf-encode's rgb2yuv.comp; whole-word writes so no 8-bit-storage feature is needed): import dmabuf -> dispatch CSC into the exportable buffer -> CUDA de-strides both planes into a pooled two-plane NV12 buffer. PUNKTFUNK_NV12 (default-on) now covers LINEAR; a CSC failure latches RGB for the stream (mid-frame fallback, no dropped frame); 4:4:4 LINEAR sessions stay RGB (never silently subsample). New ImportKind::LinearNv12 rides the existing worker IPC (appended last per the wire-tag rule); cursor stays downstream (blend_nv12). Validated: .21 clippy -D warnings (pf-zerocopy/pf-capture/host+nvenc) + 17 zero-copy tests. Owed: on-glass gamescope session (visual + dmon sm% check). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
22b352c1da |
chore(release): bump workspace version to 0.13.0
apple / swift (push) Successful in 1m19s
audit / bun-audit (push) Successful in 13s
audit / cargo-audit (push) Successful in 2m8s
ci / web (push) Successful in 53s
release / apple (push) Successful in 5m45s
ci / docs-site (push) Successful in 1m3s
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 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 40s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
ci / bench (push) Successful in 5m26s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
apple / screenshots (push) Successful in 6m27s
windows-host / package (push) Successful in 15m6s
arch / build-publish (push) Successful in 10m50s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m1s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m58s
android / android (push) Successful in 16m0s
docker / deploy-docs (push) Successful in 11s
flatpak / build-publish (push) Failing after 8m9s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m11s
deb / build-publish (push) Successful in 13m4s
ci / rust (push) Successful in 19m37s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m6s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m10s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m26s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m51s
The eight W6 leaf crates hardcoded 0.12.0 instead of inheriting the workspace version — switched to version.workspace = true so the next bump is one line again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
85bc5b9a3f |
refactor(host/W6.2): extract the Linux zero-copy GPU plumbing into the pf-zerocopy leaf crate
linux/zerocopy/* (CUDA context/buffers + EGL/Vulkan dmabuf import + the isolated import worker) and linux/dmabuf_fence.rs move wholesale into crates/pf-zerocopy, so the coming pf-frame vocabulary crate (FramePayload::Cuda owns a DeviceBuffer) and the pf-encode/pf-capture subsystem crates can reach the GPU plumbing without the host orchestrator in between (plan §W6). Content stays Linux-only; the crate compiles to an empty lib elsewhere, so dependents carry a plain dependency. drm_fourcc deliberately does NOT move: it consumes the frame vocabulary (PixelFormat), which sits ABOVE pf-zerocopy — it lives with capture for now and moves into pf-frame next. cuda's ffi re-export bumps pub(crate)->pub (the raw CUdeviceptr vocabulary is consumed across the crate boundary by the encode backends). A crate::zerocopy shim module keeps every existing path valid until capture/encode themselves move out. Verified: Linux clippy -D warnings (pf-zerocopy --all-targets + host nvenc,vulkan-encode,pyrowave --all-targets) + 17/17 pf-zerocopy tests + 321/321 host tests; Windows clippy nvenc,amf-qsv --all-targets Finished exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |