Commit Graph

19 Commits

Author SHA1 Message Date
enricobuehler 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>
2026-07-17 09:41:15 +02:00
enricobuehler 265554b755 refactor(host/W4): carve the EGL blit's GL plumbing into egl/gl.rs
apple / swift (push) Successful in 1m22s
apple / screenshots (push) Successful in 4m43s
windows-host / package (push) Successful in 9m2s
ci / web (push) Successful in 55s
android / android (push) Successful in 11m56s
ci / docs-site (push) Successful in 53s
decky / build-publish (push) Successful in 18s
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 11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (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 8s
arch / build-publish (push) Successful in 10m51s
ci / bench (push) Successful in 5m25s
deb / build-publish (push) Successful in 12m0s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m33s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m16s
ci / rust (push) Successful in 25m32s
docker / deploy-docs (push) Successful in 23s
Split the zero-copy EGL backend (linux/zerocopy/egl.rs, 1208 lines) into a
facade + egl/gl.rs (plan §W4 / §3.2). gl.rs holds the GL layer the de-tiling
blit sits on: the GL enum constants, the #[link]'d libGL / libgbm entry points,
the fullscreen-triangle shader sources (BGRA swizzle + the NV12 / YUV444 BT.709
convert passes), and the shader/program compile helpers. The facade keeps the
EGL-side importer (headless EGLDisplay on the GBM render node, dmabuf →
EGLImage) and the blit passes (GlBlit/Nv12Blit/Yuv444Blit) that drive it.

Pure move; no behavior change. Linux clippy --all-targets + fmt green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 23:18:40 +02:00
enricobuehler cb7091e1d5 refactor(host/W4): carve the raw CUDA driver-API FFI into cuda/ffi.rs
ci / web (push) Successful in 51s
ci / docs-site (push) Successful in 53s
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 21s
apple / swift (push) Successful in 1m20s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 29s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 46s
ci / bench (push) Successful in 5m53s
apple / screenshots (push) Successful in 4m37s
docker / deploy-docs (push) Successful in 29s
windows-host / package (push) Successful in 8m54s
deb / build-publish (push) Successful in 12m42s
arch / build-publish (push) Successful in 16m49s
android / android (push) Successful in 17m18s
ci / rust (push) Successful in 18m31s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m8s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m51s
Split the zero-copy CUDA backend (linux/zerocopy/cuda.rs, 1843 lines) into a
facade + cuda/ffi.rs (plan §W4 / §3.2). ffi.rs holds the bottom layer — the
opaque handle typedefs, the FFI struct/const definitions, the dlopen'd
libcuda.so.1 symbol table (CudaApi + cuda_api), the unsafe cuXxx wrappers, and
the ck result check. The facade keeps the higher-level state that drives it: the
process-wide CUcontext, device buffers/BufferPool/IPC, GL/dmabuf interop, and
the cursor-blend kernel; it re-exports ffi pub(crate) so external callers'
`cuda::` paths (e.g. cuda::CUdeviceptr) are unchanged.

Pure move; no behavior change. Linux clippy --all-targets + fmt green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 23:15:13 +02:00
enricobuehler d381cdf7f4 fix(host): NVENC open-failure resilience — backoff, failed-open hygiene, self-diagnosis
ci / web (push) Successful in 49s
ci / docs-site (push) Successful in 51s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
apple / swift (push) Successful in 1m9s
decky / build-publish (push) Successful in 18s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
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 9s
ci / bench (push) Successful in 5m48s
docker / deploy-docs (push) Successful in 24s
apple / screenshots (push) Successful in 5m18s
windows-host / package (push) Successful in 9m16s
arch / build-publish (push) Successful in 10m53s
android / android (push) Successful in 11m58s
deb / build-publish (push) Successful in 18m7s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m19s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m28s
ci / rust (push) Successful in 24m56s
Field report (Linux direct NVENC): after a codec switch, every session open
failed with NV_ENC_ERR_INVALID_VERSION until the host process was restarted —
so the poisoned state is per-process, not a driver install issue. On-hardware
investigation (RTX 5070 Ti, 610.43.03) could not reproduce it with clean codec
cycles, dirty teardowns, or open/destroy storms, but established the failure
class: the driver enforces a per-process concurrent-session cap (12 there,
status INCOMPATIBLE_CLIENT_KEY; other branches report differently) whose
exhaustion is exactly this signature — persistent open failures healed only by
a process restart. Harden every path that can feed or mask that state:

* Rebuild backoff: the in-place encoder-rebuild retries slept one frame
  interval, so all 5 attempts burned within ~40 ms at 120 Hz — no driver-side
  transient (deferred teardown of the previous session, engine reset) can
  clear that fast. Exponential backoff 100 ms → 1.6 s (~3 s total) so
  transients heal instead of killing the session.
* Destroy-on-failed-open (Linux + Windows, all four open sites): the NVENC
  docs require NvEncDestroyEncoder even when OpenEncodeSessionEx FAILS — the
  driver may have allocated the session slot before erroring. Without it a
  retry burst against a transient leaks slots toward the cap, converting the
  transient into permanent exhaustion.
* Teardown: a destroy_encoder failure (a session slot the driver may keep) is
  now logged with its status instead of silently discarded.
* One-shot self-diagnosis on a failed session open (Linux): retry the raw open
  on a fresh dedicated CUDA context and log which of the three causes applies
  — shared-context poisoned (fresh works), driver-level skew/exhaustion/GPU
  loss (fresh fails the same way), or CUDA itself unhealthy (no fresh context)
  — so the next field report pinpoints the root cause with zero reporter
  effort.

On-hardware regression tests (RTX box .21, all green): codec-switch reopen
cycle (H265→AV1→H265→H264→H265), dirty teardown with in-flight encodes, and
the full open-failure→diagnosis→in-place-recovery path via real session-cap
exhaustion. Existing RFI/reconfigure/4:4:4 smokes still pass; clippy clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 01:57:36 +02:00
enricobuehler b50b698078 fix(host/linux): satisfy clippy -D warnings on the cursor-blend path
ci / web (push) Successful in 1m1s
ci / docs-site (push) Successful in 1m12s
android / android (push) Successful in 12m33s
decky / build-publish (push) Successful in 24s
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 9s
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 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 31s
ci / bench (push) Successful in 6m53s
arch / build-publish (push) Successful in 15m27s
docker / deploy-docs (push) Successful in 11s
ci / rust (push) Failing after 17m35s
deb / build-publish (push) Successful in 11m47s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m18s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m52s
windows-host / package (push) Successful in 14m15s
apple / swift (push) Successful in 4m48s
apple / screenshots (push) Has been cancelled
The Linux clippy leg has been red since 5249d31d (cursor-as-metadata):
that push was verified fmt-green but the -D warnings clippy step (which
only compiles the Linux/CUDA target) was not. Five findings:

- capture/linux/mod.rs: the spa_meta_bitmap field-read unsafe block had
  no adjacent SAFETY comment (the preceding one documents the pointer
  arithmetic block, not this deref).
- zerocopy/cuda.rs: the cuModuleGetFunction unsafe block's SAFETY comment
  sat before the enclosing closure instead of adjacent to the block.
- zerocopy/cuda.rs: blend_argb/blend_yuv444/blend_nv12 tripped
  too_many_arguments (9/7) — geometry+cursor-size+offset params that a
  struct would only unpack at the call site; allow, matching the crate's
  existing use of the attribute.

Unblocks the 0.12.0 release (main must be green before the tag).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 18:51:35 +02:00
enricobuehler 5249d31dfa feat(host/linux): cursor-as-metadata — pointer in gamescope streams, no perf hit
ci / web (push) Successful in 46s
ci / docs-site (push) Successful in 1m8s
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 18s
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.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
windows-host / package (push) Successful in 8m37s
ci / bench (push) Successful in 5m47s
ci / rust (push) Failing after 8m24s
docker / deploy-docs (push) Successful in 22s
arch / build-publish (push) Successful in 11m26s
android / android (push) Successful in 12m54s
deb / build-publish (push) Successful in 12m8s
apple / swift (push) Successful in 5m53s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m30s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m31s
apple / screenshots (push) Successful in 19m34s
gamescope draws its pointer on a hardware DRM cursor plane that never enters
the framebuffer feeding its PipeWire capture node, so captured frames arrive
cursorless. Rather than force the producer's Embedded full-frame composite,
request the pointer as PipeWire SPA_META_Cursor and composite it ourselves —
a ≤256×256 blit into the encoder-OWNED surface, never the compositor's
read-only dmabuf.

Capture (capture/linux/mod.rs, capture.rs):
- choose_cursor_mode() gates on available_cursor_modes(): Metadata > Embedded
  > Hidden (defaults Embedded on query error — never silently lose the cursor).
  Applied on both the plain and remote-desktop portal paths.
- build_cursor_meta_param() adds a SPA_PARAM_Meta pod requesting SPA_META_Cursor
  (bitmap up to 256x256) to the connect params on every path.
- CursorState parses spa_meta_cursor (id 0 = hidden; position - hotspot; bitmap
  re-read only when bitmap_offset != 0), normalizing RGBA/BGRA/ARGB/ABGR.
  Updated in .process before the corrupted/size-0 skip so cursor-only Mutter
  buffers still track movement.
- CapturedFrame gains cursor: Option<CursorOverlay> (Arc rgba + serial) riding
  the GPU (Dmabuf/Cuda) payloads; the CPU de-pad path composites inline.

GPU composite into each zero-copy backend's owned surface:
- Vulkan Video + PyroWave: folded into the shared rgb2yuv.comp CSC shader —
  cursor sampled and alpha-mixed over RGB before the YUV convert (correct
  chroma, no extra pass). binding 3 (combined image sampler) + 16B push
  constant, per-slot cursor image uploaded only on serial change. spv regenerated.
- CUDA/NVENC: real on-GPU kernel (cursor_blend.cu -> cursor_blend.ptx,
  compute_75 Turing baseline, JIT-forward) with blend_argb/blend_yuv444/
  blend_nv12 (BT.709 limited, matching the shader). Loaded via the hand-rolled
  libcuda fn-table; blended into the ring InputSurface after copy, degrading to
  no-cursor on any failure — never drops a frame.

VAAPI (AMD/Intel fallback) deferred: Vulkan Video already covers those GPUs;
blind libva struct-layout FFI shouldn't ship unverified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 17:19:55 +02:00
enricobuehler 93093f3cf9 feat(encode): direct-SDK NVENC on Linux (CUDA input) with real RFI
ci / rust (push) Failing after 1m1s
ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 1m9s
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.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 57s
apple / swift (push) Successful in 4m42s
ci / bench (push) Successful in 7m37s
docker / deploy-docs (push) Successful in 22s
flatpak / build-publish (push) Successful in 6m51s
windows-host / package (push) Successful in 14m26s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m59s
arch / build-publish (push) Successful in 16m48s
deb / build-publish (push) Successful in 17m14s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m49s
android / android (push) Successful in 18m24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m28s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m36s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m1s
release / apple (push) Successful in 22m56s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m40s
apple / screenshots (push) Successful in 18m53s
Phase 5.2 of design/encoder-recovery-hardening.md (design/linux-direct-nvenc.md).
The Linux NVIDIA host encodes through libavcodec `hevc_nvenc`, which structurally
cannot express `nvEncInvalidateRefFrames` — so every FEC-unrecoverable loss is a
full IDR and, since the client freezes-until-reanchor, a per-loss freeze for
RTT+IDR-encode. This ports the Windows raw-NVENC backend to
NV_ENC_DEVICE_TYPE_CUDA over the shared CUcontext so Linux NVIDIA gets the same
real RFI + F2 recovery-anchor + reset() stall lever + HDR-SEI/Main10 plumbing.

New `encode/linux/nvenc_cuda.rs` (`NvencCudaEncoder`):
- runtime-loaded entry table via `dlopen libnvidia-encode.so.1` (never link-time,
  mirroring the zerocopy::cuda libcuda loader) — one binary still starts on
  AMD/Intel Linux boxes and falls through to VAAPI/software;
- session on the shared CUcontext (zerocopy::cuda::context());
- an encoder-owned ring of registered CUDADEVICEPTR input surfaces
  (zerocopy::cuda::InputSurface + a contiguous-NV12 allocator), each captured
  DeviceBuffer device→device copied in via the existing copy_* helpers — mirrors
  the libav recycled-hwframe-pool copy, so zero regression vs today;
- config/RFI/anchor/reset ported from the Windows backend; sync-only (NVENC async
  is Windows-only, so that whole subsystem is dropped);
- Main10/HDR-SEI wired but inert until a Linux P010 capture path (Phase 5.1).

Wired behind PUNKTFUNK_NVENC_DIRECT (default OFF) in open_nvenc_probed; the Windows
path is untouched (no shared extraction in v1). Two on-hardware `#[ignore]` smokes
added.

Validated on .21 (RTX 5070 Ti, driver 610.43.03): builds on Linux under ci-check,
clippy-clean, full host suite 272/0, NV12 smoke (8 AUs, real invalidate_ref_frames
+ recovery_anchor on a P-frame) and YUV444 FREXT smoke (6 AUs, chroma_444) green;
Windows compile unaffected. Owed: the client-in-the-loop matrix (RFI-survives-ABR,
reset() heal, A/B vs libav) and the default flip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 11:18:21 +02:00
enricobuehler 0dacb37088 feat(linux): zero-copy 4:4:4 — the EGL worker converts to planar YUV444 on the GPU
A 4:4:4 session no longer falls to the CPU path (SHM capture + swscale
RGB→YUV444P + re-upload — the fps-ceiling triple tax). The zero-copy worker
grows a Yuv444Blit: three full-res R8 GL passes (the proven BT.709
coefficients; studio or full range per PUNKTFUNK_444_FULLRANGE, read by both
processes so pixels and VUI flip together) into ONE stacked 3-plane pitched
CUDA allocation — which keeps the worker↔host wire and IPC single-plane. The
encoder copies the planes into ffmpeg's yuv444p CUDA surface and hevc_nvenc
emits Range-Extensions 4:4:4 natively.

ImportKind::Tiled444 is APPENDED to the worker protocol (a worker outliving a
replaced host binary must keep the old tags stable; an old worker just errors
the import, which the fail machinery already handles). A 4:4:4 session on a
LINEAR/gamescope capture — no convert wired there — fails with a clear message
instead of letting hevc_nvenc silently subsample. caps().chroma_444 now keys
off the session (it missed the GPU path when keyed off the swscale's
existence).

Live-verified on the CachyOS VM (RTX 5070 Ti): per-frame "imported to CUDA
yuv444=true", stream Rext/yuv444p/bt709 in both tv and pc range, no CPU-path
warning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 18:09:28 +02:00
enricobuehler f68f6bc590 fix(linux): zerocopy worker survives the host binary being replaced on disk
apple / swift (push) Successful in 1m10s
ci / web (push) Successful in 53s
ci / docs-site (push) Successful in 1m29s
apple / screenshots (push) Successful in 5m39s
windows-host / package (push) Successful in 7m45s
ci / bench (push) Successful in 5m30s
decky / build-publish (push) Successful in 18s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
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 7s
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 1m4s
arch / build-publish (push) Successful in 11m21s
docker / deploy-docs (push) Successful in 11s
android / android (push) Successful in 15m23s
deb / build-publish (push) Successful in 13m39s
ci / rust (push) Successful in 17m30s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m7s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m56s
A pacman canary upgrade under a running host (0.5284→0.5338, 09:24 today)
unlinked /usr/bin/punktfunk-host; current_exe() then readlinked to
"<path> (deleted)", every worker spawn failed ENOENT, and each session
silently fell back to the CPU linear-copy capture — observed as the box
"regressing" to ~90 fps at 5-7 MP until a service restart.

- RemoteImporter::spawn now pins a read fd to /proc/self/exe (once, kept for
  the process lifetime) and execs the worker via /proc/self/fd/<n>. The magic
  link names the running image's inode, not its path, so the spawn survives
  replacement/deletion — and the worker is always byte-for-byte the host's own
  build, so a mid-upgrade spawn can't hit a worker-protocol skew either. If
  the fd draws number 3 (the worker's socket slot — the pre-exec dup2 would
  clobber it) it is re-numbered; if /proc is unavailable the old path-based
  spawn remains as fallback.
- argv[0] is set to "punktfunk-host" and the worker prctl-renames its comm to
  "pf-zerocopy" — exec-by-fd-path would otherwise show a bare fd number in ps
  and top.
- zerocopy-probe now also spawns the worker (handshake + modifier query), so
  the probe catches spawn-level breakage, not just FFI/GPU bring-up.

Verified end-to-end on the dev box: probe with the binary unlinked mid-run
(/proc/self/exe → "(deleted)") still spawns the worker and reports all 13
modifiers. New unit tests cover the pinned spawn and the deleted-file exec;
the latter retries ETXTBSY (fs::copy's write fd leaks into other tests'
forked children until their execs clear it — a copy-then-exec harness
artifact, not a production concern).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:34:13 +02:00
enricobuehler 9814368c8c feat(host): enable GPU zero-copy by default on all backends
Now that the per-capture worker subprocess makes an NVENC EGL/CUDA driver
fault survivable (design/zerocopy-worker-isolation.md), the reason the NVENC
zero-copy path stayed opt-in is gone. zerocopy::enabled() now defaults ON for
both GPU backends (was ON VAAPI / OFF NVENC). Fallbacks are intact: VAAPI's
one-shot CPU auto-downgrade (VAAPI-gated, never trips for NVENC) and NVENC's
per-capture fallback + worker-death latch.

Reframe the shipped host.env examples and setup guides to rely on the default
rather than force PUNKTFUNK_ZEROCOPY=1 (an explicit =1 skips the VAAPI
auto-downgrade).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 10:33:43 +00:00
enricobuehler 1d17524151 feat(host): isolate the zero-copy GPU import in a worker process
The tiled EGL/GL→CUDA import crashed the whole host (SIGSEGV inside
libnvidia-eglcore via cuGraphicsMapResources) when the compositor
invalidated an imported dmabuf mid-map — reproduced on the Bazzite F44
Game→Desktop switch (design/zerocopy-hardening-handoff.md). A driver
SIGSEGV is uncatchable in-process, so the whole EglImporter (tiled
EGL/GL→CUDA and LINEAR Vulkan→CUDA) now runs in a per-capture
`zerocopy-worker` subprocess: dmabuf fds go over a SEQPACKET socketpair
(SCM_RIGHTS, sent once per buffer keyed by dmabuf st_ino; NeedFd resend
self-heals cache desync), frames come back as CUDA-IPC pooled device
buffers (still zero-copy, +one socket RTT/frame). Worker death poisons
the capturer so the existing capture-loss rebuild runs — the host
survives; 3 consecutive deaths latch the GPU import off (CPU/SHM path).
PUNKTFUNK_ZEROCOPY_INPROC=1 keeps the old in-process import for
debugging/A-B.

Also fixed along the way: a failed *tiled* import no longer falls
through to the CPU mmap de-pad (which scrambled tiled bytes; LINEAR
keeps the fallback); Nv12Blit dropped its GL textures while still
CUDA-registered (unregister now runs first); GlBlit had no Drop at all
(GL objects leaked per size change); VkBridge's per-fd src cache is now
invalidated on renegotiation/eviction instead of never.

Design: design/zerocopy-worker-isolation.md. Unit tests: 14 new
(protocol fd-passing, worker dispatch, client handshake/death/NeedFd,
death latch). On-glass validated on the RTX 5070 Ti/GNOME box (.21):
the worker path streams at p50 1.30 ms (NV12, 1800 frames 0-mismatched,
parity with the in-process path), and a kill -9 of the worker
mid-stream is survived by the host and recovered — poison -> capture
lost, rebuilding pipeline in place -> a fresh worker in ~185 ms ->
streaming resumes (2385 frames, 0 mismatched). A real KWin
compositor-crash repro is still pending (a worker kill -9 is strictly
harsher, so it corroborates).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 10:33:43 +00:00
enricobuehler 98b405a25c feat(host/linux): amdgpu session clock pin — gpuclocks grows the AMD arm
nvclocks.rs -> gpuclocks.rs. PUNKTFUNK_PIN_CLOCKS=1 now also pins every
amdgpu card's power_dpm_force_performance_level to high for the host
lifetime (prior level restored on exit) — the measured AMD encode-
latency lever: VCN per-frame time doubles when a 60fps paced trickle
lets clocks sag (8 -> 4.4ms/frame at 1440p on the 780M with clocks
hot). Root-gated by sysfs ownership; non-root degrades to a logged
recipe (validated live on the AMD box). Opt-in stays deliberate:
box-wide power-management override, wrong on battery/Deck.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:33:56 +00:00
enricobuehler d7e01ad92e feat(host/vaapi): submit-split instrumentation + async_depth knob (depth 1 stays default)
Chasing the 8ms submit at 1440p on the 780M: the sampled PUNKTFUNK_PERF
split (push/pull/send) shows desc+buffersrc at ~5us, hwmap-import+VPP
CSC at ~0.2-0.5ms, and avcodec_send_frame owning the rest — so neither
a VA-surface import cache nor CSC overlap would help. Two facts landed:
(1) async_depth>=2 in libavcodec's vaapi_encode is a structural
+1-frame latency (frame N's packet only materializes when N+1 queues;
measured 18ms vs 8.3ms p50 at depth 1) — depth 1 stays the default,
PUNKTFUNK_VAAPI_ASYNC_DEPTH exists for pixel rates beyond the ASIC's
serial budget, and poll() now does a bounded in-flight wait so a deeper
depth still ships the AU as soon as the ASIC finishes. (2) The residual
send_frame block tracks GPU CLOCKS, not the ASIC: ~8ms/frame at a 60fps
duty cycle vs ~4.4ms at 120fps pacing vs 3.5ms back-to-back (270fps CLI
benchmark, even at -async_depth 1) — the clock-sag fix lands in
gpuclocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:33:56 +00:00
enricobuehler fe315ed2d4 feat(host/linux): default the tiled zero-copy path to GPU NV12 (NVENC fed native YUV)
A/B'd on the Bazzite box (RTX 5070 Ti, KWin 6.6, driver 595, 1080p60
over the LAN): pixel-correct decode (full desktop, no tint/banding),
latency-neutral idle (p50 1.47ms RGB vs 1.52ms NV12, both 2400/2400
frames), CPU-neutral — and it deletes NVENC's internal RGB->YUV CSC
from the SM/3D engine a game saturates (video 40%+SM 15% -> video
26%+SM 2% measured on Windows). Matches the Windows host default.
PUNKTFUNK_NV12=0 restores the RGB feed; LINEAR/gamescope captures are
unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:33:56 +00:00
enricobuehler 04d1b08745 feat(host/linux): NVIDIA clock hygiene — P2-cap driver profile + opt-in NVML clock floor
Two halves of the easy-scene p99 lever (host-latency plan Tier 1B):
CudaNoStablePerfLimit application profile (no root; NVIDIA's supported
opt-out of the CUDA/NVENC P2 memory-clock clamp, raw key 0x166c5e=0 per
open-gpu-kernel-modules#333, shipped for obs/Discord in R595) installed
into ~/.nv/nvidia-application-profiles-rc.d/ keyed on procname, opt-out
PUNKTFUNK_NV_PROFILE=0; and PUNKTFUNK_PIN_CLOCKS=1 arming an NVML
SetGpuLockedClocks(TDP, UNLIMITED) core-clock floor (base floor, boost
headroom — never a max pin) held for the host lifetime, reset-on-start
self-healing a crashed run's stale pin, NO_PERMISSION degrading to a
logged sudoers/oneshot recipe. libnvidia-ml is dlopen'd like libcuda —
no link-time dependency, clean no-op off NVIDIA.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:33:56 +00:00
enricobuehler d6bf91a708 feat(host/capture): zero-copy by default on VAAPI hosts (dmabuf passthrough)
PUNKTFUNK_ZEROCOPY unset now defaults ON when the encode backend is
VAAPI — a stock AMD/Intel install gets the LINEAR-dmabuf -> GPU-CSC path
instead of three full-frame CPU touches (measured on the 780M at 1440p:
0.8s vs 7.9s CPU per 600 frames, pixel-identical). NVENC stays opt-in.
A dmabuf offer the compositor never accepts latches a one-shot downgrade
so the pipeline rebuild renegotiates on the CPU offer; explicit =1 keeps
erroring loudly. The EGL->CUDA importer is no longer built on VAAPI
backends (an NVIDIA box forced to PUNKTFUNK_ENCODER=vaapi now correctly
takes the passthrough instead of producing CUDA frames the encoder
rejects), and a VAAPI session landing on the CPU path warns with the
reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:33:56 +00:00
enricobuehler b54695739d docs(host): prove every unsafe block in the Linux FFI files + gate them (unsafe-proof program 2/N)
Continues the structural unsafe-proof program (every unsafe carries a documented
proof of soundness; the file gains #![deny(clippy::undocumented_unsafe_blocks)]
so it stays proven). This batch covers all 10 remaining pure-Linux files
(104 blocks), each proof stating the REAL invariant — not boilerplate:

  zerocopy/cuda.rs (26)   leaked process-lifetime libcuda fn-ptr table; opaque
                          CUcontext never dereferenced; free-exactly-once via the
                          Arc<Mutex<PoolInner>> ownership graph; dmabuf fd take/close split
  zerocopy/egl.rs (18)    eglGetProcAddress'd procs with the GL context current;
                          EGLImage liveness; the two-call modifier-query bounds
  zerocopy/vulkan.rs (4)  copy-bounds arithmetic (src_size>=span); Send = thread
                          confinement to the punktfunk-pipewire thread
  dmabuf_fence.rs (4)     poll/ioctl/close fd liveness + ownership
  capture/linux/mod.rs (16)  spa_data repr(transparent) cast; null-checked spa
                          derefs; single-loop-thread buffer ownership until requeue
  inject/linux/gamepad.rs (10)  uinput ioctl request-number ↔ struct-size match
                          (static-asserted); InputEventRaw no-padding for the byte cast
  encode/linux/vaapi.rs (15) + encode/linux/mod.rs (9)  ffmpeg object ownership/
                          free ladders; VAAPI/DRM graph; Send = single-thread transfer
  inject/linux/wlr.rs (2), vdisplay/linux/kwin.rs (1)

No memory-unsafety SUSPECT blocks were found — the unsafe is sound. The vaapi
agent did flag two real AVBufferRef *leaks* (not UB) in DmabufInner::open; marked
inline with NOTE(leak) and addressed in a follow-up.

Verified: cargo clippy -p punktfunk-host --all-targets -- -D warnings is clean
(each file's deny gate hard-errors on any undocumented block).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:00:30 +00:00
enricobuehler 97802b4b57 docs(host): prove every unsafe block in drm_sync.rs + gate it (unsafe-proof program 1/N)
Start of the structural unsafe-proof program (per the "every unsafe needs a
documented proof of soundness" goal): each `unsafe` block gets an accurate
`// SAFETY:` proof of WHY it is sound, and the file gains
`#![deny(clippy::undocumented_unsafe_blocks)]` so the proof requirement is
permanently enforced (a future undocumented unsafe in this file fails CI).

drm_sync.rs (10 blocks: libc open/ioctl/clock_gettime/close + 3 in tests): each
proof states the real invariant — fd liveness/ownership, the ioctl request number
encoding the matching struct size, the `&mut req` being a live correctly-sized
`#[repr(C)]` struct, and (for the timeline ioctls) the `handles`/`points` arrays
outliving the synchronous call with `count_handles` matching their length.

The gate grows file-by-file (CI stays green; undone files don't carry the lint
yet); it promotes to a crate-root deny once every file is done. ~122 Linux blocks
+ the Windows files remain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 08:35:32 +00:00
enricobuehler fced221684 refactor(windows-host): confine platform code under windows/ + linux/ folders (Goal-1 stage 6)
Move 36 platform-specific files into per-module `windows/` and `linux/` subfolders (and the
shared HID codecs into `inject/proto/`):
  capture/{windows,linux}/  encode/{windows,linux}/  inject/{windows,linux,proto}/
  audio/{windows,linux}/  vdisplay/{windows,linux}/
  src/windows/ (service, wgc_helper, win_adapter, win_display)
  src/linux/  (dmabuf_fence, drm_sync, zerocopy/)

Done with `#[path]`, NOT a module rename: every file moves into its folder while the
`crate::*::*` module names stay FLAT, so all caller paths and every internal `super::`/`crate::`
reference are unchanged — only the parent `mod` decls gained `#[path = "..."]`. This is the
codebase's existing pattern (inject's gamepad_windows) and makes the move byte-identical in
behaviour with ZERO reference churn, far lower risk than collapsing to a single
`crate::capture::windows::` namespace (that deeper rename is an optional follow-on; this delivers
the cfg-sprawl folder confinement the stage is about). Done LAST, after the semantic stages, so
the path churn didn't fight them.

Verified: Linux cargo check + clippy (-D warnings) clean; my mod-decl changes fmt-clean (the 3
remaining fmt diffs are pre-existing local-rustfmt-version skew that moved with their files); all
36 `#[path]` targets exist; no internal `#[path]`/`include!`/file-child-mod in any moved file
(the inline `mod X {` blocks are self-contained). Box build to follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:53:45 +00:00