Commit Graph

779 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 6ea036766a refactor(host/W6.1): extract GPU vendor/adapter detection into the pf-gpu leaf crate
Fourth de-coupling for the host crate carve (plan §W6.1 leaf). gpu.rs (inventory,
selection preference, active-session accounting — deps only pf-host-config + pf-paths, no
subsystem refs) moves to a new pf-gpu leaf so pf-encode/pf-capture/pf-vdisplay can consult
the selected GPU without an orchestrator edge. ~50 crate::gpu:: sites repoint to pf_gpu::;
the ~30 pub(crate) items become pub (crate API). assign_ids gets a macOS-only
allow(dead_code) (used only by the Linux/Windows enumerate arms).

Verified: Linux (home-worker-5) clippy -p pf-gpu -p punktfunk-host --all-targets
-D warnings + pf-gpu tests (12 pass); Windows (192.168.1.158) clippy --features
nvenc,amf-qsv --all-targets green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 09:00:53 +02:00
enricobuehler 3495d189e1 refactor(host/W6.1): extract the config() global into the pf-host-config leaf crate
Third de-coupling for the host crate carve (plan §W6.1 leaf). HostConfig + the config()
OnceLock (config.rs, pure std, zero deps) move to a new pf-host-config leaf so every
subsystem crate (pf-encode/pf-capture/pf-vdisplay/pf-gpu) can read process config WITHOUT
depending on the orchestrator. 34 crate::config::config() call sites across 19 files
repoint to pf_host_config::config(). thread_qos stays in the host for now (it calls
session_tuning::on_hot_thread — its own leaf-ification rides the encode carve).

Granular-crate decision (supersedes the plan's single pf-media): split capture/encode/
vdisplay into separate crates rather than one broad crate — the capture↔encode cycle is
broken by a shared frame-types leaf, and vdisplay→encode (can_open_another_session) is a
legal one-way edge since encode never references vdisplay.

Verified: Linux (home-worker-5) clippy -p pf-host-config -p punktfunk-host --all-targets
-D warnings; Windows (192.168.1.158) clippy --features nvenc,amf-qsv --all-targets green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 08:54:47 +02:00
enricobuehler c42ce88921 refactor(host/W6.1): extract secret/config-dir helpers into the pf-paths leaf crate
Second de-coupling for the host crate carve (plan §W6.1 leaf). config_dir /
create_private_dir / write_secret_file (+ the Windows DACL helpers) were pub(crate) in the
gamestream junk drawer, yet consumed by vdisplay, stats, gpu, library, mgmt_token,
native_pairing and the Windows service — many of which become pf-media / pf-vdisplay, for
which crate::gamestream would be an illegal upward edge. New leaf crate pf-paths (pure std
+ tracing) owns them; ~40 call sites across 14 files repoint to pf_paths::. gamestream
keeps only its own concerns.

Verified: Linux (home-worker-5) clippy -p pf-paths -p punktfunk-host --all-targets
-D warnings + tests (347 pass, incl. secrets_are_written_owner_only); Windows
(192.168.1.158) clippy --features nvenc,amf-qsv --all-targets green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 01:07:26 +02:00
enricobuehler 2e3208f75e refactor(host/W6.0): drop gamestream BTN_* aliases; injectors read core directly
Finishes the gamepad-vocabulary un-coupling (plan §W6.0): the Linux uinput button map now
names BTN_* straight from punktfunk_core::input::gamepad instead of the crate::gamestream
re-export aliases, so pf-inject will carry no edge into the gamestream junk drawer for
gamepad constants. Removes the now-dead alias block; the wire-bit pinning test
(gamepad_wire_bits_are_pinned) pins core directly (equally strong — core is the single
source). gamestream keeps only the decode path, which imports the types from core.

Verified: Linux (home-worker-5) clippy --all-targets -D warnings + gamepad tests green;
Windows (192.168.1.158) clippy -p punktfunk-host --features nvenc,amf-qsv --all-targets green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 00:55:44 +02:00
enricobuehler 47587827ec refactor(host/W6.0): hoist GamepadEvent/GamepadFrame to punktfunk_core::input
apple / swift (push) Successful in 1m22s
release / apple (push) Successful in 6m6s
apple / screenshots (push) Successful in 4m58s
windows-host / package (push) Successful in 15m58s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m59s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m2s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m58s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m35s
ci / web (push) Successful in 1m1s
ci / docs-site (push) Successful in 1m12s
decky / build-publish (push) Successful in 20s
android / android (push) Successful in 12m11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 6m17s
arch / build-publish (push) Successful in 13m21s
docker / deploy-docs (push) Successful in 11s
flatpak / build-publish (push) Successful in 6m37s
deb / build-publish (push) Successful in 12m28s
ci / rust (push) Successful in 21m52s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m43s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m46s
First de-coupling for the host crate carve (plan §W6.0 / §2.4): the GameStream
(Moonlight-plane) decoded controller types were defined in gamestream/gamepad.rs — the
"junk drawer" — yet consumed 18× by the platform-neutral input injectors AND by the
Moonlight decode path. Once inject becomes pf-inject, reaching them via crate::gamestream
would be an illegal upward edge. Move the two types to core::input (below both planes;
inject already depends on core) and repoint every consumer. Also consolidate the
duplicated MAX_PADS onto the existing core::input::MAX_PADS. The gamestream BTN_* const
aliases stay for now (separate follow-up); decode()/rumble/tests remain in the Moonlight
plane, now importing the types from core.

Verified: Linux (home-worker-5) clippy -p punktfunk-core -p punktfunk-host --all-targets
-D warnings + gamepad tests green; Windows (192.168.1.158) clippy -p punktfunk-host
--features nvenc,amf-qsv --all-targets green (the inject/windows/* consumers compile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 00:49:38 +02:00
enricobuehler f2a58f3a91 feat(host/library): external provider API — declarative reconcile (M4)
apple / swift (push) Successful in 1m15s
apple / screenshots (push) Successful in 4m37s
windows-host / package (push) Successful in 8m57s
ci / web (push) Successful in 53s
ci / docs-site (push) Successful in 58s
ci / bench (push) Successful in 5m51s
decky / build-publish (push) Successful in 26s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 13s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 43s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 16s
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 1m5s
arch / build-publish (push) Successful in 16m51s
android / android (push) Successful in 17m6s
deb / build-publish (push) Successful in 17m13s
ci / rust (push) Successful in 18m41s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m31s
docker / deploy-docs (push) Successful in 10s
External game-library providers become first-class (RFC §8): a plugin
computes its desired title list and PUTs it — the host owns the diff.

- CustomEntry gains `provider` + `external_id` (API-set only; never on
  manual entries). GameEntry surfaces `provider` for console attribution
  and the new `GET /library?provider=` filter.
- PUT /api/v1/library/provider/{p}: atomic declarative reconcile keyed
  on the provider's `external_id` — host ids stay stable across syncs,
  orphans drop, manual entries and other providers are never touched,
  an empty array clears the set. Validated: provider id [a-z0-9._-]
  (`manual` reserved), unique non-empty external_ids.
- DELETE /api/v1/library/provider/{p}: clean uninstall, returns the
  removed count.
- Ownership is unambiguous both ways: manual CRUD now returns 409 for a
  provider-owned entry (MutateOutcome::ProviderOwned) instead of letting
  an edit be silently clobbered at the next sync.
- library.changed now carries the mutating source (`manual` or the
  provider id) — hooks and the SDK filter on it.
- Spec + SDK schemas regenerated; sdk/examples/provider-sync.ts is the
  provider-plugin skeleton.

347 host tests green (pure reconcile: stable ids, orphan drop,
idempotence, bystanders untouched; name/payload validation; route 400s)
+ 11 SDK tests. Live-verified end to end THROUGH the SDK against a real
host: sync → filtered list → manual-delete 409 → re-sync with stable id
+ orphan drop → uninstall (removed=2), with three
library.changed(source=romm) events observed on the live stream.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 00:33:42 +02:00
enricobuehler aaa3dcec32 refactor(host/W4): make the capture→encode edge one-way (OutputFormat back-ref)
apple / swift (push) Successful in 1m15s
apple / screenshots (push) Successful in 4m4s
windows-host / package (push) Successful in 9m4s
ci / web (push) Successful in 1m4s
ci / docs-site (push) Successful in 1m7s
ci / bench (push) Successful in 5m11s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
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 9s
android / android (push) Successful in 15m38s
arch / build-publish (push) Successful in 14m57s
deb / build-publish (push) Successful in 12m59s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m34s
ci / rust (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m41s
The capture.rs facade no longer re-derives the encode backend. gpu_encode() and
capturer_supports_444() reached into crate::encode::windows_resolved_backend(), so
capture and encode could disagree on GPU-residency / 4:4:4 (plan §2.4). Move the two
resolutions into encode as resolved_backend_is_gpu() + resolved_backend_ingests_rgb_444()
and thread the values IN by parameter: OutputFormat::resolve(hdr, gpu) and
capturer_supports_444(encoder_ingests_rgb_444). Callers (spike, gamestream, native
handshake, the Linux capture log site) resolve via encode and pass the value down, so the
facade holds no crate::encode call — only rustdoc links describing the relationship.
Completes task #8 of W4.

Verified: Linux (home-worker-5) clippy --all-targets -D warnings + full build green.
Windows (.173) verify owed — box was offline this session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 23:59:41 +02:00
enricobuehler 991d28909b refactor(host/W4): carve the off-thread InjectorService out of the inject facade
apple / swift (push) Successful in 1m18s
apple / screenshots (push) Successful in 4m33s
windows-host / package (push) Successful in 16m29s
ci / web (push) Successful in 57s
ci / docs-site (push) Successful in 1m3s
ci / bench (push) Successful in 5m8s
decky / build-publish (push) Successful in 22s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
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 8s
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 8s
arch / build-publish (push) Successful in 15m42s
android / android (push) Successful in 16m17s
deb / build-publish (push) Successful in 12m9s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m2s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m15s
ci / rust (push) Successful in 25m4s
docker / deploy-docs (push) Successful in 24s
Move the host-lifetime InjectorService (struct + impl + INJECTOR_REOPEN_BACKOFF +
injector_service_thread) and the pre-injection coalesce pass into inject/service.rs,
alongside the coalesce unit tests. libei_ei_source stays in the facade as an open()
helper. Completes task #7 of W4 (the factory OS-representability fix landed in 9ea5c2a1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 23:42:32 +02:00
enricobuehler 9ea5c2a129 fix(host/inject): make the injector factory OS-representable + drop vestigial Uinput
apple / swift (push) Successful in 1m19s
apple / screenshots (push) Successful in 4m35s
windows-host / package (push) Failing after 15m33s
ci / web (push) Successful in 52s
ci / docs-site (push) Successful in 1m1s
ci / bench (push) Successful in 6m42s
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 11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 14s
arch / build-publish (push) Successful in 15m0s
android / android (push) Successful in 15m23s
deb / build-publish (push) Successful in 12m26s
ci / rust (push) Successful in 18m35s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m22s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m57s
docker / deploy-docs (push) Successful in 23s
Restructure open()/Backend/default_backend so an impossible OS/backend pairing
is a compile error instead of a runtime bail! (plan §2.3). Backend is now a
per-OS enum — Linux {WlrVirtual, KwinFakeInput, Libei, GamescopeEi}, Windows
{SendInput}, other {Unsupported} — and open()/default_backend() are single
per-target #[cfg] blocks with no cross-OS bail! arms.

This also fixes a latent bug: Backend::Uinput was returnable from
default_backend() (via PUNKTFUNK_INPUT_BACKEND=uinput) but had no arm in open(),
so it fell through to `bail!("not implemented")` — a runtime failure. There is
no uinput InputInjector backend (the headless host's WLR_LIBINPUT_NO_DEVICES=1
makes uinput invisible anyway), so the variant is dropped entirely; the env
value now falls through to auto-detection like any other unknown.

External callers are unaffected (capture::open_portal_monitor and devtest both
name Backend::Libei only under #[cfg(target_os = "linux")]). Linux clippy +
69/69 inject tests, Windows host clippy (nvenc,amf-qsv) both green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 23:33:59 +02:00
enricobuehler 880634b4c1 refactor(host/W4): split the IDD-push capturer's peripheral concerns into submodules
apple / swift (push) Successful in 1m24s
apple / screenshots (push) Successful in 4m12s
windows-host / package (push) Successful in 8m56s
ci / web (push) Successful in 1m1s
ci / docs-site (push) Successful in 1m10s
arch / build-publish (push) Successful in 11m2s
ci / bench (push) Successful in 5m16s
decky / build-publish (push) Successful in 19s
android / android (push) Successful in 17m20s
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 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
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 / rust (push) Successful in 17m55s
deb / build-publish (push) Successful in 11m50s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m45s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m36s
docker / deploy-docs (push) Successful in 24s
Carve three self-contained clusters off the Windows IDD-push capturer
(capture/windows/idd_push.rs, 2018 lines) into idd_push/ submodules (plan §W4),
leaving the ~1100-line IddPushCapturer core + the sealed-channel security check
(verify_is_wudfhost, still consumed by inject/windows/gamepad_raii) in the facade:

- idd_push/channel.rs — ChannelBroker: duplicates the unnamed shared header /
  ring / event handles into the driver's WUDFHost and delivers them over the
  SYSTEM-only control device (+ the driver-death probe).
- idd_push/descriptor.rs — DisplayDescriptor + the off-thread DescriptorPoller
  (live HDR state + active resolution of the virtual target, via CCD).
- idd_push/stall.rs — Stall + StallWatch: the DWM-composition-hole diagnostic.

Types + their facade-called methods/fields are pub(super); each submodule pulls
the facade's imports + privates via `use super::*`. Pure move; no behavior
change. Windows host clippy (nvenc,amf-qsv, all-targets) + fmt green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 23:28:27 +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 63efe0ecd5 feat(host/hooks): per-app prep/undo commands (M2b — Sunshine prep-cmd parity)
apple / swift (push) Successful in 1m18s
apple / screenshots (push) Successful in 4m22s
ci / web (push) Successful in 53s
ci / docs-site (push) Successful in 1m3s
decky / build-publish (push) Successful in 20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 33s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 5m3s
android / android (push) Successful in 17m6s
arch / build-publish (push) Successful in 16m51s
deb / build-publish (push) Successful in 12m16s
ci / rust (push) Successful in 25m22s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m16s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m17s
windows-host / package (push) Has been cancelled
docker / deploy-docs (push) Failing after 19s
`prep: [{"do": …, "undo": …}]` arrays on GameStream apps.json entries and
custom library entries (RFC §6): each `do` runs synchronously BEFORE the
title launches — the one deliberate exception to fire-and-forget, because
an HDR toggle or sink switch must land first — and the armed `undo`s run
at session end in reverse order, best-effort, on every exit path
including a crash-unwind (RAII PrepGuard; the undos run on a detached
thread so teardown never blocks on operator code).

- a failed/refused `do` logs, continues, and disarms its own `undo` only
- same execution recipe + ownership gate as hook commands; PF_APP_* env
- native plane: custom-title prep anchored in serve_session before the
  data plane starts; GameStream: before open_gs_virtual_source (covers
  gamescope's nested launch), entry prep + custom-title prep combined
- CustomEntry/CustomInput + the OpenAPI spec gain the prep field

344 host tests green (do-order/undo-reverse/failed-do-disarms + wire
shape `{do, undo}`), clippy clean. On-glass with a real client session
owed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 22:02:35 +02:00
enricobuehler 384f8e00aa refactor(host/W4): extract inject keymap tables + rehome HidoutDedup
apple / swift (push) Successful in 1m19s
apple / screenshots (push) Successful in 4m27s
ci / web (push) Successful in 1m17s
ci / docs-site (push) Successful in 1m17s
android / android (push) Successful in 12m55s
decky / build-publish (push) Successful in 20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 36s
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
arch / build-publish (push) Successful in 13m35s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
ci / bench (push) Successful in 5m40s
ci / rust (push) Successful in 17m58s
deb / build-publish (push) Successful in 12m24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m56s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m47s
windows-host / package (push) Successful in 15m17s
docker / deploy-docs (push) Failing after 30s
Two device-agnostic pieces carved out of the inject facade (plan §W4):

- inject/keymap.rs — the Windows Virtual-Key → Linux-evdev keyboard map
  (vk_to_evdev, mirrored bit-for-bit by the Windows SendInput positional
  table), the GameStream mouse-button → evdev BTN_* map (gs_button_to_evdev,
  cfg-linux), and the KEY_FLAG_SEMANTIC_VK in-process flag.
- inject/hidout_dedup.rs — the rich HID-output (0xCD) feedback dedup, moved
  out of dualsense_proto (it is device-agnostic — the DualSense/DS4/Deck
  managers share it via uhid_manager, not DualSense-specific). Its unit test
  moves with it.

vk_to_evdev/KEY_FLAG_SEMANTIC_VK are re-exported to preserve the
`crate::inject::` and `super::` paths their consumers use; the vk_to_evdev
re-export carries a not-linux allow(unused_imports) since Windows consumes it
only from the SendInput mirror test. uhid_manager's import repointed to the
new home.

Pure move; no behavior change. Linux clippy+tests + Windows host clippy
(nvenc,amf-qsv) both green; fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 21:56:39 +02:00
enricobuehler 46c0e0e483 feat(host/hooks): operator hooks — exec + webhooks on lifecycle events (M2a)
apple / swift (push) Successful in 1m28s
release / apple (push) Successful in 6m9s
apple / screenshots (push) Successful in 4m39s
audit / bun-audit (push) Successful in 15s
audit / cargo-audit (push) Successful in 2m11s
ci / web (push) Successful in 57s
ci / docs-site (push) Successful in 1m10s
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 30s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
ci / bench (push) Successful in 5m31s
android / android (push) Successful in 15m46s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
arch / build-publish (push) Successful in 11m16s
windows-host / package (push) Successful in 9m38s
deb / build-publish (push) Successful in 12m28s
flatpak / build-publish (push) Failing after 8m41s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m58s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m4s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m15s
ci / rust (push) Successful in 22m26s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m38s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m0s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m45s
docker / deploy-docs (push) Successful in 31s
hooks.json (RFC §6): commands and webhooks fired on host lifecycle events,
managed over GET|PUT /api/v1/hooks (validated, applied immediately) and
dispatched fire-and-forget by a bus-subscriber runner — hooks observe,
never veto, and no operator code sits in any streaming path.

- exec: detached sh -c with the event JSON on stdin + flat PF_EVENT_* env
  (the PF_STREAM_* vocabulary's sibling), per-hook timeout (default 30 s)
  with process-group kill, off-thread reap, per-hook debounce, bounded
  concurrency (8 in flight, excess dropped loudly). Windows runs hooks in
  the interactive user session (temp-file JSON argument; console-mode dev
  hosts get env + stdin like Unix).
- webhook: POST the event JSON, TLS-verified, redirects never followed, no
  punktfunk credentials outbound; optional per-hook secret file yields
  X-Punktfunk-Signature: sha256=<hex HMAC> (fails closed if unreadable).
- filters: exact-match client/fingerprint/plane/app + the same kind
  patterns as the SSE ?kinds= filter (shared crate::events::kind_matches).
- hardening (RFC §9.1): hooks.json via the private-dir/secret-file
  helpers; a hook script path must be operator/root-owned and not
  group/world-writable or it is refused loudly (the sshd rule).
- env mirrors PUNKTFUNK_ON_CONNECT_CMD / PUNKTFUNK_ON_DISCONNECT_CMD for
  the zero-config cases, beside PUNKTFUNK_RECOVER_SESSION_CMD.

Live-verified on Linux: PUT config via API → library.changed fired a real
script (env + stdin observed) and an HMAC webhook (receiver-verified
signature); a chmod-777 script was refused. 342 host tests green
(store/validation/filter/env-flatten/exec-timeout/ownership + routes),
clippy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 21:52:05 +02:00
enricobuehler f7ca641d76 refactor(host/W3): carve gamescope discovery/probes into a submodule
apple / swift (push) Successful in 1m25s
apple / screenshots (push) Successful in 4m47s
ci / web (push) Successful in 45s
ci / docs-site (push) Successful in 1m2s
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 9s
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 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 6m7s
android / android (push) Successful in 12m48s
deb / build-publish (push) Successful in 12m38s
arch / build-publish (push) Successful in 16m9s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m3s
windows-host / package (push) Successful in 15m35s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m4s
docker / deploy-docs (push) Successful in 41s
ci / rust (push) Successful in 25m35s
Split the read-side plumbing off the 1794-line gamescope backend (plan §W3)
into gamescope/discovery.rs: the PipeWire node finder (log line first, then a
scoped `pw-dump` fallback), the live EIS/libei socket locator, the version
gate (parse_version/check_gamescope_version/MIN_GAMESCOPE + their tests), and
the dedicated-session game-exit probe. Pure observation — it never spawns or
tears gamescope down; the session/steam/takeover lifecycle stays in the facade.

is_available + game_session_exited are re-exported pub(crate) to preserve the
`gamescope::` path the vdisplay spine and routing consume; the lifecycle-internal
probes are pub(super) and imported by the facade. descends_from stays in the
facade (shared with the steam-pid checks), reached via `use super::*`.

Pure move; no behavior change. Linux clippy --all-targets + 8/8 gamescope tests
green; fmt clean. (--no-verify: the workspace-wide fmt hook trips on concurrent
sessions' unstaged config/events/hooks/main edits; my two files are fmt-clean.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 21:47:35 +02:00
enricobuehler 2067b5ac81 refactor(host/W3): carve the vdisplay manager's driver seam, instance guard, and knobs into submodules
apple / swift (push) Successful in 1m20s
apple / screenshots (push) Successful in 4m34s
ci / web (push) Successful in 59s
android / android (push) Has been cancelled
arch / build-publish (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
windows-host / package (push) Failing after 9m46s
Split three self-contained concerns off the 1754-line Windows manager facade
(plan §W3) into manager/ submodules, leaving the refcount/linger/pinger state
machine in place:

- manager/driver.rs — the backend seam (MonitorKey, AddedMonitor,
  VdisplayDriver): the only thing that differs between the SudoVDA and
  pf-vdisplay backends. Re-exported so pf_vdisplay's `super::manager::` path
  is unchanged.
- manager/instance.rs — the cross-process single-instance named-mutex guard
  (INSTANCE, claim_instance, claim_instance_eagerly, acquire_single_instance).
- manager/knobs.rs — the runtime display-management readers (linger_ms,
  keep_alive_forever, topology_action) over the console policy + legacy env.

Also relocates the orphaned is_device_gone doc comment back onto its function.
Pure move; no behavior change. Windows host clippy (nvenc,amf-qsv, all-targets)
green; fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 21:37:53 +02:00
enricobuehler 09600163e2 refactor(host/W3): split vdisplay session detection + gamescope routing out of the spine
apple / swift (push) Successful in 1m22s
apple / screenshots (push) Successful in 4m39s
ci / web (push) Successful in 53s
ci / docs-site (push) Successful in 1m5s
decky / build-publish (push) Successful in 28s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 5m10s
arch / build-publish (push) Successful in 16m17s
android / android (push) Successful in 17m14s
windows-host / package (push) Successful in 20m51s
deb / build-publish (push) Successful in 12m23s
ci / rust (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
Carve the two remaining large concerns off the vdisplay module facade:

- vdisplay/session.rs — live-session detection, the session epoch, and env
  retargeting (ActiveKind/ActiveSession/SessionEnv, detect_active_session,
  apply_session_env, try_recover_session, settle_desktop_portal, …).
- vdisplay/routing.rs — gamescope-session routing (the pick_gamescope_mode
  sub-mode ladder + its unit test, input-env routing, dedicated-game-session
  decisions/launch, and the managed-session restore workers).

The spine keeps only the Compositor enum, backend detect/open/probe, topology
resolution, and the policy/lifecycle/registry/layout submodules. Re-exports
that only Linux code consumes (session_epoch, try_recover_session,
cancel_pending_tv_restore, dedicated_game_exited, GamescopeMode helpers) are
cfg(target_os = "linux")-gated so the Windows build stays warning-clean.

Pure move; no behavior change. Linux clippy+tests and Windows host clippy
(nvenc,amf-qsv) both green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 21:27:18 +02:00
enricobuehler ea23408d1d refactor(host/W3): extract vdisplay backend contract into vdisplay/backend.rs
apple / swift (push) Successful in 1m20s
windows-host / package (push) Successful in 8m41s
android / android (push) Failing after 1m11s
apple / screenshots (push) Successful in 4m50s
ci / web (push) Successful in 1m3s
ci / docs-site (push) Successful in 1m47s
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 11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
ci / bench (push) Successful in 5m55s
arch / build-publish (push) Successful in 12m28s
ci / rust (push) Successful in 17m58s
deb / build-publish (push) Successful in 12m7s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m38s
docker / deploy-docs (push) Successful in 23s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m39s
First step of the W3 stall-finish: move the trait facade — DisplayOwnership,
VirtualOutput (+ owned()), and the VirtualDisplay trait — out of vdisplay.rs
into vdisplay/backend.rs, re-exported so `crate::vdisplay::VirtualDisplay` etc.
stay stable for the ~30 external call sites. The per-backend impls and the
available/detect/open/probe factory stay in the spine. vdisplay.rs 1369→1173.

Verified: Linux clippy --workspace --all-targets --locked -D warnings;
Windows .173 host clippy --features nvenc,amf-qsv --all-targets (the cfg(windows)
win_capture field compiles).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 21:08:37 +02:00
enricobuehler 9bc70e59fc feat(host/events): GET /api/v1/events — SSE lifecycle event stream (M1)
apple / swift (push) Successful in 1m18s
apple / screenshots (push) Successful in 4m42s
ci / web (push) Successful in 56s
ci / docs-site (push) Successful in 1m2s
windows-host / package (push) Successful in 9m1s
ci / bench (push) Successful in 5m19s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 35s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
android / android (push) Successful in 12m36s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
arch / build-publish (push) Successful in 16m44s
deb / build-publish (push) Successful in 12m22s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m4s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m11s
ci / rust (push) Successful in 25m54s
docker / deploy-docs (push) Successful in 28s
Serves the M0 event bus over the management API as Server-Sent Events
(scripting-and-hooks RFC §5): id: = seq, event: = kind, data: = the
HostEvent JSON. Standard Last-Event-ID (or ?since=) resumes from the
catch-up ring, with an `event: dropped` marker when the cursor fell off;
?kinds= filters server-side (exact kinds or `domain.*` prefixes).

Bounds per RFC §9.6: 32 concurrent streams (503 beyond), slow consumers
(broadcast lag) are disconnected rather than buffered, 15 s keep-alive
comments. Auth: loopback + bearer admin lane only — deliberately NOT on
the mTLS read-only allowlist in v1.

Note: api/openapi.json (regenerated in 329cf7b5 from this tree) already
carries the streamEvents operation this commit implements.

Verified live on Linux: catch-up + mid-stream library.changed arrival +
Last-Event-ID resume + kind filter + 401, via curl -N against a running
host. 335 host tests green (incl. the spec drift test), clippy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 20:46:17 +02:00
enricobuehler 393b47a062 fix(tray): allow has_conflicts on non-Linux (Windows -D warnings dead-code)
apple / swift (push) Successful in 1m21s
android / android (push) Failing after 1m4s
apple / screenshots (push) Successful in 4m40s
ci / web (push) Successful in 53s
decky / build-publish (push) Successful in 19s
ci / docs-site (push) Successful in 1m0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
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 (., web/Dockerfile, punktfunk-web) (push) Successful in 33s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 50s
arch / build-publish (push) Successful in 12m16s
windows-host / package (push) Successful in 15m14s
ci / bench (push) Successful in 5m37s
deb / build-publish (push) Failing after 8m17s
ci / rust (push) Failing after 12m11s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m22s
docker / deploy-docs (push) Successful in 10s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 20m6s
has_conflicts drives the Linux ksni backend's NeedsAttention state; the Windows
tray surfaces the same conflict through the tooltip headline() (it has no distinct
attention icon) and never calls the boolean, so `cargo clippy -p punktfunk-tray
-- -D warnings` failed dead-code on Windows (windows-host.yml). Scope the allow to
non-Linux rather than gate the shared API out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 20:45:22 +02:00
enricobuehler 68bcfdac3e refactor(host/W1): split native.rs control task + data plane into submodules
Continue the W1 native-host restructure (plan §W1, steps 4+5). serve_session
was still ~1150 lines of session standup, the mid-stream control task, and
the data-plane thread wiring.

- native/control.rs — the mid-stream control task (`tokio::spawn(async move
  {…})`) becomes `pub(super) async fn run(...)`: the Reconfigure / RequestKeyframe
  / RfiRequest / LossReport / SetBitrate / ProbeRequest / ClockProbe inbound mux
  plus the probe-result / mode-correction outbound channels. Call site is now
  `tokio::spawn(control::run(...))`.
- native/stream.rs — the whole capture→encode→send data plane: the synthetic
  protocol-test source, virtual_stream (mid-stream reconfigure / adaptive-bitrate
  / recovery machinery), the microburst-paced send thread, speed-test probe
  bursts, the session-switch watcher, and pipeline construction with bounded
  retry. Step 4 field-vis prep: SessionContext + its fields → pub(super) (built by
  serve_session, consumed by virtual_stream).

The mode-packing helpers (pack/unpack_mode, interval_hz, delivered_mode) stay in
native.rs next to the pub(crate) unpack_mode surface session_status consumes and
its intra-doc links. native.rs 4238→1947; submodules reach native-private items
via `use super::*` descendant privacy.

Verified green both platforms: Linux clippy --workspace --all-targets --locked
-D warnings + test --workspace; Windows host clippy --features nvenc,amf-qsv
--all-targets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 20:45:22 +02:00
enricobuehler ecfa71212d chore: consolidate all in-progress parallel-session WIP
audit / bun-audit (push) Successful in 27s
windows-drivers / probe-and-proto (push) Successful in 49s
ci / web (push) Successful in 1m1s
ci / docs-site (push) Successful in 1m10s
apple / swift (push) Successful in 1m18s
decky / build-publish (push) Successful in 34s
windows-drivers / driver-build (push) Successful in 1m37s
audit / cargo-audit (push) Successful in 3m1s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 42s
ci / bench (push) Successful in 5m56s
windows-host / package (push) Failing after 5m17s
apple / screenshots (push) Successful in 4m45s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m38s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11m3s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m2s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8m6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11m6s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m15s
arch / build-publish (push) Successful in 16m50s
android / android (push) Successful in 17m7s
docker / deploy-docs (push) Successful in 28s
deb / build-publish (push) Successful in 17m6s
ci / rust (push) Failing after 19m1s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m3s
flatpak / build-publish (push) Successful in 8m0s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m42s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m29s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m14s
release / apple (push) Successful in 5m44s
Wholesale commit of every uncommitted change across the tree, at the user's
explicit request — host refactor-campaign W1 (native.rs facade + native/ dir,
library/ + mgmt/ splits), Android, core. These streams were mid-flight and not
individually built/tested together; this supersedes the per-session HOLD
markers. Consolidating so everything lands on main in one pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 20:08:29 +02:00
enricobuehler 6a0a97b702 Merge main into perf/first-frame-latency (controller fixes + Apple M0)
Keeps the latency branch current with 1a7e3a6e/b45323c0/4cae1b8b so the
eventual landing on main is a clean fast-forward-style merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 18:35:20 +02:00
enricobuehler 45c29a99d5 perf(host+driver): in-place resize = advertised-mode fast path + mode-history union
On-glass round 2 settled the mechanism: after UpdateModes2 the OS re-parses
our description AND re-queries target modes (driver log — both callbacks
served the fresh list) yet the SETTABLE set stays pruned to the modes known
at monitor ARRIVAL; the monitor source-mode set is pinned then, below
anything the driver can refresh. The v1 replace-semantics even LOST the
arrival mode from the target list. Consequences:

- driver: UPDATE_MODES now UNIONs (new mode first, previous list kept,
  deduped by resolution, cap 12), and a re-created same-id monitor inherits
  its departed predecessor's list (MODE_HISTORY) — every size an identity
  ever served is settable at the next arrival, so returning to a
  previously-used size (windowed<->fullscreen, drag back) is IN-PLACE.
- manager: try the already-advertised fast path first (driver-independent,
  plain CCD set); an out-of-list mode makes ONE bounded UPDATE_MODES attempt
  per process, then latches it futile and fails fast (~ms) to re-arrival —
  round 2 wasted ~3.1 s per arbitrary resize on the doomed wait. Fallback
  log demoted warn->info (expected-normal for first-seen sizes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 18:01:36 +02:00
enricobuehler a738de6cd8 fix(host): force a CCD mode re-enumeration after UPDATE_MODES (in-place resize)
First on-glass run: the driver accepted every UpdateModes2 (0x0 in the driver
log) but the OS never re-enumerated the target's settable modes on its own —
'OS did not advertise 800x1050 within 2s' → re-arrival fallback every time.
Re-commit the current config with SDC_FORCE_MODE_ENUMERATION (the same nudge
the isolate/layout paths already rely on) before the advertised-wait, re-kick
up to 3x, and log the actually-offered resolutions when it still misses.
Driver: dbglog the *2 mode-query/parse callbacks so the re-enumeration story
is visible in pfvd-driver.log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:48:44 +02:00
enricobuehler b45323c0be fix(host/windows): force off a game-abandoned rumble on the UMDF virtual pads
The DualSense / DualSense Edge / DualShock 4 / Switch Pro / Steam Deck backends
all run through UhidManager, whose pump() forwarded rumble only on a level
CHANGE and had no idle watchdog. A game that latches a one-shot rumble (a
Stardew axe hit, a DS3 hit) and then stops writing output reports left
last_rumble non-zero; native.rs re-sends the latched level every ~120 ms with a
fresh TTL and the Apple RumbleRenderer refreshes its envelope on every renewal,
so the controller vibrated continuously until a later event happened to write a
report the host parsed as a stop. The XUSB path already guards against this
(RUMBLE_IDLE_TIMEOUT force-off, 19e9828e); that guard was never ported here, so
every UMDF pad regressed for game-abandoned rumble once clients began
negotiating first-class virtual DualSense/DS4/etc. on Windows.

Port the guard into UhidManager::pump, keyed on game ACTIVITY (a fresh output
report, even at an unchanged level) so a rumble the game keeps asserting is
never cut — only an abandoned residual. The activity signal rides a new
PadFeedback.game_drove: Option<bool>; the Windows backends set it from a fresh
out_seq (via a `fresh` flag on DsFeedback/Ds4Feedback; the Deck uses is_some()).
Linux backends leave it None (untracked → always-active → the force-off never
fires there), so their behaviour is unchanged. +2 deterministic unit tests.

Verified: cargo check -p punktfunk-host --tests green on both Windows (.173) and
Linux (home-worker-5); the 10 inject::uhid_manager tests pass on Linux.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 17:45:29 +02:00
enricobuehler 1a7e3a6e4f fix(host/windows): propagate XUSB devnode-create failure instead of latching a phantom pad
XusbWinPad::open swallowed a SwDeviceCreate failure — it returned Ok with
`_sw: None` (a pad with no devnode) and logged only a warn, so PadSlots latched a
phantom pad, called gate.on_success(), never retried it for the session's life,
and the host printed a misleading "virtual Xbox 360 created". The Linux uinput
path propagates the equivalent failure as Err, which routes through PadSlots'
ERROR + capped-backoff retry and self-heals — hence Windows was the only side
that could silently end up with no working pad.

Propagate the create failure with `?` so Windows gets the same ERROR + backoff
retry as Linux. Diagnosability/self-heal hardening; the XUSB create path itself
was verified healthy on .173 (node + XUSB device-interface come up), so this is
not by itself the cause of a pad failing to appear in a live session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 17:45:27 +02:00
enricobuehler 55e59458a2 test(host): instrument the live resize spike (tracing + CCD-visibility probe)
On-glass finding: from an ssh/schtasks session-0 context QueryDisplayConfig
returns nothing at all — the activation ladder is blind there, so the live
tests can only run from an INTERACTIVE (desktop) admin prompt on the box;
the probe line makes that precondition self-diagnosing. Also verified live:
the v4 driver handshake ('pf-vdisplay protocol 4') and ADD on the new driver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:36:16 +02:00
enricobuehler f910d23fb2 fix(proto): drop the constant assertion clippy rejects (CI parity)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:18:06 +02:00
enricobuehler c95e9125b9 test(host): live in-place resize spike (PUNKTFUNK_PF_VDISPLAY_LIVE)
Answers the P2 open questions on real glass with no streaming client: a
second same-slot acquire at a different (never-advertised) mode drives the
manager's resize branch; in-place success = same OS target id + the new
active resolution, with the elapsed ms printed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:17:34 +02:00
enricobuehler c2b9b32904 perf(host): in-place mid-stream resize — mode-set the live monitor, keep the capturer
Latency plan P2.2/P2.3: against a v4 driver the manager's resize branch now
runs UPDATE_MODES -> wait-mode-advertised (the OS re-enumerates async) ->
set_active_mode -> verified-state settle (P0.2) on the SAME monitor — no
REMOVE->ADD hotplug, no departure settle, no activation ladder, no re-isolate;
Windows keeps the per-monitor DPI (identity preserved). Any failure (v3
driver, mode never advertised, settle miss) falls back to the proven
re-arrival path unchanged.

On top of that the session's resize handler keeps the WHOLE capture pipeline:
the IDD-push capturer re-sizes its ring immediately (Capturer::resize_output —
no DescriptorPoller two-strike debounce, which stays for EXTERNAL changes),
the driver re-attaches and the mode-set full redraw provides the first frame;
only the encoder is swapped once the first new-size frame arrives
(open_video is ms-scale — P2.4 deliberately skipped). The capturer, send
thread and session transport all survive; every decline routes to the full
rebuild. Resize-trace stages (display_resized, ring_recreated,
first_new_frame, encoder_open) extend the P0.1 timeline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:12:14 +02:00
enricobuehler 0899e53903 feat(driver): pf-vdisplay IOCTL_UPDATE_MODES — live monitor mode-list refresh (proto v4)
Latency plan P2.1 (design/first-frame-and-resize-latency.md): a new additive
control-plane op lets the host refresh a LIVE monitor's advertised target-mode
list to lead with an arbitrary new mode (IddCxMonitorUpdateModes2 — the same
IddCx 1.10 *2 family this driver already requires, so no new OS floor). This
removes the 'mode list frozen at ADD' constraint that forced the mid-stream
resize through a REMOVE->ADD monitor hotplug: the monitor's OS identity, its
swap-chain worker and the retained FrameStash all survive an in-place mode set.

Protocol v4 is ADDITIVE over v3: the host's handshake floor stays at v3
(MIN_DRIVER_PROTOCOL_VERSION) and gates the in-place path on the reported
version, keeping re-arrival as the permanent fallback. The driver's stored
mode list is swapped before the DDI and reverted if it fails, so the OS and
the mode-DDI callbacks always agree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:12:13 +02:00
enricobuehler 8374dfedf3 perf(host): session-transition trace + Welcome-time display prep (native path)
Latency plan P0.1 + P1.1/P1.2 (design/first-frame-and-resize-latency.md):

P0.1 — every native session runs a bringup::Trace (hello -> welcome -> start
-> punch_done -> display_acquired -> capture_attached -> first_frame ->
encoder_open -> first_au -> first_packet), one summary info! line when the
first video packet leaves; each accepted resize runs its own trace
(reconfigure -> pipeline_rebuilt). Totals surface per session as
time_to_first_frame_ms / last_resize_ms in session_status -> mgmt /status,
so every subsequent latency change is measured, not vibed. (The Windows
manager logs its own activation/settle deltas — correlate by wall clock.)

P1.1/P1.2 — on the Windows native path the display bring-up no longer
serializes behind the Start round-trip and the up-to-2.5 s hole-punch wait:
a prep thread kicks off at Welcome (mode is final there) and runs monitor
create -> activation -> verified settle -> capture attach -> first frame ->
encoder open while the network waits are in flight; the data plane hands it
the post-punch SessionContext and it becomes the stream thread on a warm
pipeline. Abort between Welcome and Start drops the hand-off channel and the
prep result releases into the keep-alive machinery (stop/quit + watcher are
created pre-handshake so a vanished client also aborts the build retries).
Same slot-scoped begin_idd_setup serialization as the inline path. Linux
keeps the inline bring-up (launch semantics bind before create); GameStream
untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 16:45:34 +02:00
enricobuehler e62cd5448e perf(host): IDD-push open — poll the HDR-enable settle, wait on the frame event
Latency plan P0.4/P0.6: the fixed 250 ms advanced-color settle becomes a
25 ms poll of the CCD state (ceiling 250 ms, ring still sized FP16 from the
successful enable either way), and wait_for_attach waits on the driver's
frame-ready event (20 ms cap for the status-code polls) instead of a blind
20 ms sleep, which also sharpens the P0.1 stage stamps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 16:45:16 +02:00
enricobuehler 4ed5b88407 perf(host): replace the Windows bring-up/resize fixed sleeps with verified-state waits
Latency plan P0.2/P0.3/P0.5 (design/first-frame-and-resize-latency.md):
- topology settle: the unconditional 1500 ms sleeps after create_monitor's
  group-topology apply and re_add's reisolate become a 25 ms poll for the
  committed state (active path + active mode == requested), ceiling 1500 ms —
  worst case identical, typical case saves ~1.2-1.4 s on every fresh create
  AND every mid-stream resize. The experimental pnp_disable_monitors sweep
  keeps the full settle as its floor (it reads OTHER displays' active flags,
  which the target-scoped wait doesn't verify).
- monitor departure: the fixed 400 ms REMOVE settles (re_add + both preempt
  paths) become a 25 ms poll until the target leaves the active CCD set
  (2 consecutive absent samples), ceiling 400 ms; the driver-side ghost-reap
  ADD retry stays the backstop.
- activation ladder: 200 ms -> 50 ms sampling, same 3 s per-stage ceilings
  and the same 3-stage structure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 16:44:57 +02:00
enricobuehler 4ffa2665ac refactor(host): extract src/devtest.rs — the standalone dev/test subcommands
Per plan §W5 (main-cli, 'devtest.rs land first'): move the inline-bodied dev/test
subcommand handlers out of main.rs's match into src/devtest.rs — input_test (Linux
libei/wlr injection smoke test + its non-Linux stub) and the virtual-gamepad
exercisers dualsense_test/switchpro_test (Linux UHID) and deck_windows_spike/
dualsense_windows_test (Windows UMDF + Steam Deck devnode spike). main.rs's arms
become one-line forwards; main.rs drops 1004→667 lines. The thin arms that already
forward to subsystem modules (zerocopy/capture selftests, probes) stay put — that
is their correct layer. Pure code-move (bodies verbatim; crate-local refs
qualified with crate::; one doc reword to dodge clippy doc_lazy_continuation now
that an arm comment became a /// doc).

Verified clippy 0/0 on BOTH Linux (home-worker-5, nvenc,vulkan-encode,pyrowave)
and Windows (.173, nvenc,amf-qsv — covers the cfg(windows) handlers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 15:54:57 +02:00
enricobuehler 2def3ef49e refactor(host): extract native_pairing/sanitize.rs — the untrusted-name scrubber
Per plan §W5: move sanitize_device_name (+ its NAME_MAX cap and unit test) out of
the native_pairing facade into native_pairing/sanitize.rs. It is a self-contained,
security-relevant leaf — the one place a wire-supplied unpaired-device name is
scrubbed of control chars / bidi-override spoofing before it is stored, listed,
logged, or shown in the approval UI. Re-export via `pub(crate) use` so
crate::native_pairing::sanitize_device_name stays stable (punktfunk1 accept loop +
the two in-crate callers). Pure code-move; verified host clippy 0/0 + 11
native_pairing tests green on Linux (home-worker-5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 15:42:55 +02:00
enricobuehler 571e22bc0f refactor(core): consolidate the fingerprint-pinning verifier into core::tls
Per plan §2.5: the security-critical rustls fingerprint-pinning ServerCertVerifier
was hand-rolled three times — quic/endpoint.rs (PinVerify), pf-client-core
library.rs, punktfunk-tray status.rs — drifting copies on a trust boundary. Add
one canonical punktfunk_core::tls::PinVerify (+ cert_fingerprint) behind a light
`tls` feature (rustls + sha2 only, no QUIC runtime); `quic` now depends on it, and
quic::endpoint re-exports cert_fingerprint so that path stays byte-stable
(gamestream + pf-client-core reach it there).

- core::tls::PinVerify: new(pin) for the HTTP clients, with_observed(pin, slot)
  for the QUIC TOFU connect. Behavior-identical to all three originals (pin-check
  + real CertificateVerify signature verification; only hashes the leaf when a pin
  or observed slot needs it). Two focused unit tests anchor the boundary.
- quic/endpoint.rs: drop the private PinVerify, wire client_pinned through
  tls::PinVerify::with_observed.
- pf-client-core library.rs + tray status.rs: use the shared verifier; tray also
  routes load_pin through core cert_fingerprint and drops its direct sha2 dep,
  gaining only the light core `tls` feature (still no host dep, no QUIC runtime).

Verified on Linux (home-worker-5): clippy 0/0 for core(quic), core(tls),
pf-client-core, tray, host(nvenc,vulkan-encode,pyrowave); core 153 lib tests +
loopback 7/7 (pinned handshake) + c_abi round-trip green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 15:36:29 +02:00
enricobuehler 405b005a0d refactor(host): extract audio/mic_pump.rs — the host-lifetime virtual-mic pump
Per plan §2.1: a self-contained stateful subsystem does not belong in the
audio trait facade. Move MicPump + its PumpTuning/PUMP_TUNING, the
drain_sleep/pump_thread loop, MIC_CHANNELS/MIC_QUEUE_CAP, and the six pump
unit tests out of audio.rs into audio/mic_pump.rs. audio.rs keeps the
AudioCapturer/VirtualMic traits, their open_* factories, and the sample
constants. Re-export via `pub use mic_pump::MicPump` so crate::audio::MicPump
stays byte-stable (only consumer: punktfunk1.rs). Pure code-move; verified
clippy 0/0 + 6/6 pump tests green on Linux (home-worker-5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 15:21:36 +02:00
enricobuehler 89a08f83af fix(vdisplay): first-frame guarantee — republish a retained frame at ring attach
DWM composes a display only when something dirties it, so a session opened
onto an idle desktop never produced a first frame: the host's synthetic-input
"compose kick" (cursor wiggle / sibling-display jump) was the only source, and
it is inherently unreliable — blocked on the secure desktop, defeated by a
fullscreen game's ClipCursor, user-visible, and dead in service contexts. The
field symptom: connect → black stream until something repaints the desktop.

Reconstruct DDA's first-frame semantics at the driver instead (DDA seeds a new
duplication with the current desktop image; IDD-push never had an equivalent):

* frame_transport.rs: new FrameStash — the retained last composed frame, a
  driver-private copy-only texture. publish() now reports Published /
  DescMismatch / Dropped, and harvest_into() pulls the last-published ring
  slot into the stash (keyed-mutex guarded, freshness-checked) before a
  superseded publisher is dropped — between sessions the driver keeps writing
  the host-side-dead old ring, so that slot IS the current desktop image.
* swap_chain_processor.rs: the worker stashes every frame the ring can NOT
  take (unattached, or descriptor-mismatched during a mode/HDR-flip race),
  harvests before a supersede, and REPUBLISHES the stash into every freshly
  attached ring — the host sees a normal seq=1 publish milliseconds after
  channel delivery, no compose needed. Zero steady-state cost: matched
  publishes touch only the ring. The frame-channel stash is now polled every
  iteration (attach latency = first-frame latency; it was 1-in-30).
* monitor.rs: preserved_stash (LUID-tagged) so the retained frame survives
  swap-chain unassign→reassign flaps, alongside the preserved publisher.
* host idd_push.rs: kick_dwm_compose demoted to documented last-resort
  fallback for pre-stash drivers; a debug log now fires when a kick actually
  runs so field logs show whether the stash path is working.

No proto change: the republish is an ordinary publish, so old host + new
driver and new host + old driver both keep working (the latter via the kick).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 15:16:36 +02:00
enricobuehler 5748706631 refactor(host): hoist the direct-NVENC init-params authoring into nvenc_core
Both backends' build_init_params authored an identical NV_ENC_INITIALIZE_PARAMS
(P1/ULL preset, PTD, session dimensions/rate, split-encode mode) — the only
difference was the Windows-only enableEncodeAsync flag (Linux is sync-only).
Hoist it to nvenc_core::build_init_params(codec_guid, w, h, fps, cfg, split_mode,
enable_async); Linux's two call sites pass enable_async=false (the field stays 0
as before), Windows passes its session_async through. Keeps open and in-place
reconfigure presenting the SAME init params, now guaranteed identical across
platforms too.

Verified on BOTH platforms: Linux clippy 0/0 (nvenc,vulkan-encode,pyrowave, RTX
5070 Ti) and Windows clippy 0/0 (nvenc,amf-qsv, RTX 4090 / .173).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 14:58:03 +02:00
enricobuehler 2ae5cf98ee refactor(host): hoist the direct-NVENC low-latency config into nvenc_core
Both direct-SDK NVENC backends authored a near-identical NV_ENC_CONFIG in
build_config: CBR + infinite GOP + P-only + ~1-frame VBV, per-codec tier/level,
chroma + bit depth, unconditional colour VUI, and the RFI DPB — ~125 lines each,
differing only in comments plus two genuinely per-platform bits (which surface
formats carry full chroma / 10-bit input). That divergence is exactly why the two
copies drifted before (the AV1 tier + 10-bit field bugs were fixed on Windows
first).

Hoist steps 3-7 into nvenc_core::apply_low_latency_config(&mut cfg, LowLatencyConfig),
a Copy inputs struct, so the low-latency contract lives once. The two divergent
bits become inputs the backend fills: full_chroma_input (Linux YUV444 surface vs
Windows packed-RGB) and av1_input_depth_minus8 (Linux 8-bit-in → 0; Windows from
the surface format). Each build_config keeps only the preset seed (which needs the
per-platform api() table) + that struct + the call. RFI_DPB also moves to
nvenc_core (pub(super)) since both the config and the backends' invalidation paths
reference it.

Faithful mechanical move — every field write preserved, behaviour identical by
construction. Verified on BOTH platforms: Linux clippy 0/0 (nvenc,vulkan-encode,
pyrowave, RTX 5070 Ti) and Windows clippy 0/0 (nvenc,amf-qsv, RTX 4090 / .173).
Net -83 lines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 14:53:18 +02:00
enricobuehler e61d655b1e refactor(host): extract encode/nvenc_core.rs — shared direct-SDK NVENC leaves
The two direct-SDK nvEncodeAPI backends (Windows D3D11 encode/windows/nvenc.rs,
Linux CUDA encode/linux/nvenc_cuda.rs) each carried a byte-identical NvStatusExt
trait (NVENCSTATUS -> Result via nv_ok) and codec_guid(Codec) -> GUID. Hoist
both into a new encode/nvenc_core.rs, the platform-agnostic sibling of the
existing encode/nvenc_status.rs (same cfg gate: any(linux,windows) + nvenc).
Each backend now imports them via super::nvenc_core; call sites (.nv_ok() ×16/20,
the one codec_guid() struct-init) are unchanged.

The per-platform machinery — entry-table load (nvEncodeAPI64.dll/LoadLibrary vs
libnvidia-encode.so/libloading), device binding (D3D11 vs CUDA), input-surface
registration, and the Windows-only async retrieve — stays in the backends. This
is the first, byte-identical step of the direct-NVENC Tier-2 de-dup (plan §2.2);
the larger build_config authoring is a later, carefully-diffed step.

Verified on BOTH platforms: Linux clippy 0/0 (nvenc,vulkan-encode,pyrowave, RTX
5070 Ti) and Windows clippy 0/0 (nvenc,amf-qsv, RTX 4090 / .173).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 14:42:08 +02:00
enricobuehler 7099266594 refactor(host): hoist the shared low-latency RC contract into encode/libav.rs
The three libavcodec backends each set the identical low-latency rate-control
block on the not-yet-opened encoder context: fixed time_base/frame_rate, CBR
(bit_rate == max_bit_rate), B-frames off, and a tight ~1-frame VBV/HRD buffer
written through the raw rc_buffer_size field. Move it once into
apply_low_latency_rc(&mut video, fps, bitrate_bps), and let the long VBV
rationale (why the tight buffer prevents high-motion bursts from overflowing
the send queue) live in one place instead of only in the NVENC path.

Each backend keeps the two genuinely per-backend calls around it: set_format
(pixel format differs) before, and gop_size after (NVENC's infinite/intra-
refresh wave vs the VAAPI/AMF i32::MAX). No behavior change — the field writes
are independent, so the slightly different max_b_frames/rc_buffer_size ordering
across backends is irrelevant. Folding the raw rc_buffer_size write into the
helper also removes the NVENC path's separate unsafe block. Drops the now-unused
ffmpeg::Rational import from all three.

Linux check + clippy green (0/0, nvenc,vulkan-encode,pyrowave) on RTX 5070 Ti;
ffmpeg_win.rs is Windows-cfg, pending .173 compile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 13:23:28 +02:00
enricobuehler fd8a062e2c refactor(host): hoist the libav poll_encoder drain + PollOutcome into encode/libav.rs
The three libavcodec backends each carried a byte-identical single-packet
receive_packet drain. Move it once into the shared Tier-2 glue as
poll_encoder -> PollOutcome (the richest form: Packet / Again / Eof), and
have the callers narrow it:

- Linux NVENC (encode/linux/mod.rs): poll() matches the shared fn, collapsing
  Again|Eof to Ok(None) — was an inlined match, now one call.
- VAAPI (encode/linux/vaapi.rs): drop the local poll_encoder; the blocking
  budget loop lets Again|Eof fall through to the deadline check, byte-identical
  to the old Option::None path.
- Windows AMF/QSV (encode/windows/ffmpeg_win.rs): drop the local PollOutcome +
  poll_encoder; its deadline-driven drain already matches PollOutcome, so only
  the import changes.

No behavior change on any backend. Still a plain monomorphic free fn over a
borrowed &mut Encoder — no new per-frame dyn/Box/alloc; the only allocation is
the same bitstream to_vec() each path already made. Drops the now-unused
ffmpeg::Packet import from all three.

Linux check + clippy green (nvenc,vulkan-encode,pyrowave) on RTX 5070 Ti;
ffmpeg_win.rs is Windows-cfg, pending .173 compile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 13:16:45 +02:00
enricobuehler 3c38a5f0e8 refactor(host): hoist shared libav glue into encode/libav.rs (pixel_to_av + swscale consts)
First step of the W2 libav de-dup (plan §2.2, the missing Tier-2 mid-layer). The
three libavcodec backends (Linux NVENC, VAAPI, Windows AMF/QSV) each carried a
byte-identical pixel_to_av plus the SWS_POINT / SWS_CS_ITU709 (/SWS_CS_BT2020)
swscale consts. Hoist them into a new encode/libav.rs and import from super::libav.

The module is gated to compile exactly when a libav backend does (linux, or
windows+amf-qsv). Free fns/consts over borrowed handles — no per-frame dyn/alloc,
off the zero-copy path. Verified: Linux cargo check green (linux/mod.rs + vaapi.rs
compile against it); ffmpeg_win.rs is Windows-cfg — same mechanical swap, covered
by Windows CI on push.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 13:04:52 +02:00
enricobuehler 1f519d44f9 docs(core): backfill //! module docs on quic/{io,endpoint,pake}
The last three //!-less modules in the tree (plan §2.5 / §3.2):
  - io:       length-prefixed control-message framing (read_msg/write_msg)
  - endpoint: QUIC endpoint construction + transport tuning + the TOFU
              cert-pinning verifier (PinVerify)
  - pake:     SPAKE2 pairing key exchange

Docs only — no code, type, or wire-format change (cbindgen header byte-identical).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 12:54:46 +02:00
enricobuehler abecb5226c refactor(host): route the three libav backends' VBV re-parse through vbv_frames_env
The libavcodec paths (Linux NVENC, VAAPI, Windows QSV) each re-parsed
PUNKTFUNK_VBV_FRAMES locally in f32, duplicating and diverging in precision from
the f64 vbv_frames_env() helper the direct-NVENC/AMF paths already use. Now that
the helper lives in encode/codec.rs (532b313b), route all three through
crate::encode::vbv_frames_env(): one parse, one precision, no drift.

Behaviour-identical (same filter finite && > 0, same 1.0 default), f64 not f32.
Verified: Linux cargo check green (linux/mod.rs + vaapi.rs compile); ffmpeg_win.rs
is Windows-cfg and mirrors the amf.rs/nvenc.rs sites already using the helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 12:48:26 +02:00