3ba19f28a23dd1b3d69230eb260f3e041459211a
65 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1fcf9e11ec |
fix(video): honor the signaled CSC matrix end-to-end + tvOS HDR presentation
Clients derive Y'CbCr->RGB from the stream's SIGNALED matrix x range x depth via shared csc rows (Rust csc_rows + Swift CscRows) instead of hardcoded 709/2020 - a BT.601-signaled stream (a Linux host's RGB-input NVENC) no longer renders with a constant hue error. Host-side signaling made honest across NVENC/VAAPI/openh264/GameStream and the session plan's chroma/bit-depth. Decoded color-bar fixtures (601/709 x limited/full) pin the math in tests on both cores. Same presenter, tvOS HDR: tvOS has no Metal EDR API and a bare PQ colorspace tag composites UNTONE-MAPPED (the "overblown" Apple TV report), so HDR now splits on the display's live EDR headroom - PQ passthrough when the per-session AVDisplayManager mode switch landed (a real HDR10 output tone-maps itself), else an in-shader PQ->SDR tone-map (203-nit reference white, extended-Reinhard 1000-nit knee, 2020->709) into the proven SDR layer config. The 10-bit stream keeps its full decode depth either way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d1770c3476 |
refactor(host): shared send-pacing policy for the native and GameStream video planes
Networking-audit deferred plan §5. Both planes spread a frame's wire
packets across a time budget in chunked bursts; the schedule logic,
PUNKTFUNK_VIDEO_DROP loss injection, and percentile helper were duplicated
between punktfunk1::paced_submit and gamestream::stream::spawn_sender. Now
one host-local send_pacing::pace_frame carries the policy; each plane keeps
its exact historical parameterization and its own syscall layer (GSO
Session vs sendmmsg over the RTP socket — policy shared, plumbing not):
native burst_bytes = PUNKTFUNK_PACE_BURST_KB (microburst stage),
fixed 16-packet chunks, budget = 0.9 × time-to-deadline
gamestream no burst stage, bounded steps (≤ 12, chunk ≥ 16, the old
pace_layout), fixed budget = 0.75 × frame interval
Deterministic-schedule unit tests pin both parameterizations against
verbatim transcriptions of the legacy math (burst split, chunk layout,
step counts — including pace_layout's historical test anchors) and the
sleep-target formula (GameStream's legacy per_step form agrees to
≤ steps/2 ns; the unified fraction form is used for both). Deliberate
sub-observable normalizations, all on test-knob or ns-scale paths:
PUNKTFUNK_VIDEO_DROP is now parsed once per process and clamped to 1..=90
on the GameStream plane too (was per-stream, unclamped), and the native
sleep floor comparison is now >= (was >, differs only at exactly 500 µs).
Validation:
- 263 host tests green, incl. the end-to-end sender_delivers_batches
(spawn_sender → pace_frame → sendmmsg, byte-identical delivery)
- PUNKTFUNK_VIDEO_DROP FEC sweep at 5 % and 8 % injected wire loss:
all 11 punktfunk1 integration tests (full host↔client roundtrips
through send_loop → paced_submit) recover and pass
- pending: one real Moonlight smoke session against this build (the
legacy-plane timing gate) — recipe handed to the operator
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
baa04d2d24 |
style: cargo fmt over the networking-audit changes
rustfmt pass over the files the deferred-plan items touched (pinned toolchain 1.96.0); no semantic change. cargo fmt --all --check now clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9afcbcd307 |
feat(transport): Windows DSCP via qWAVE flows — PUNKTFUNK_DSCP now real on the wire there
Networking-audit deferred plan §4 (the qos.rs follow-up). On Windows set_tos_v4 succeeds but the stack strips the mark without a qWAVE flow, so PUNKTFUNK_DSCP=1 was a silent wire no-op there. Now (Apollo/Sunshine's approach): QOSCreateHandle once per process; QOSAddSocketToFlow per connected media socket — video → QOSTrafficTypeAudioVideo, audio → QOSTrafficTypeVoice (QOS_NON_ADAPTIVE_FLOW) — then best-effort QOSSetFlow(QOSSetOutgoingDSCPValue, 40/48) to pin the exact CS5/CS6 the other platforms mark. The pin lands for elevated processes (the host runs as the SYSTEM service — exactly where the video egress is) or under the "allow non-admin DSCP" policy; otherwise the traffic-type default marking stands (still WMM-useful). Gating + contract unchanged: opt-in via dscp_enabled(), every step debug-logs and continues. set_media_qos now returns an RAII QosFlow guard (QOSRemoveSocketFromFlow on drop) that must outlive the socket's traffic: stored in UdpTransport (declared before the socket, so drop order removes the flow first) and held for the stream's scope by the GameStream video/audio senders — whose tagging moved after connect(), since qWAVE derives the flow's 5-tuple from the connected socket (behavior-neutral on Linux). Off-Windows the guard is inert and never constructed. Validated: cargo check -p punktfunk-core --target x86_64-pc-windows-msvc green (the full host can't cross-check from Linux — aws-lc-sys needs MSVC tooling; it builds on-box via deploy-host.ps1). Remaining on the next Windows pass per plan: deploy to the RTX box and pktmon/Wireshark the client side — DSCP ≠ 0 on video egress with PUNKTFUNK_DSCP=1, 0 without. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cdbdc078d6 |
perf(core): ref-based FEC encode — packetize shards reference the frame in place
Stage A of the zero-copy host packetize path (networking-audit deferred plan §1): ErasureCoder::encode now takes &[&[u8]], so Packetizer::packetize builds each block's data shards as slices straight into the frame buffer instead of allocating + copying a Vec per data shard. Only the frame's final (possibly partial) shard is staged in a reusable zero-padded scratch; blocks are consecutive shard ranges, so every other shard is a full payload-sized slice. - gf8: encode_sep() over the same Cauchy codec — parity byte-identical to nanors/Moonlight (nanors_exact_parity_vectors unchanged and green) - gf16: reed_solomon_simd::encode is already generic over AsRef<[u8]> - loss-harness sweep: recovery rates identical before/after - bench pipeline (end-to-end, host+client): gf8/64K -3.0%, gf16/64K -2.2%, gf16/1M -3.4%, gf8/1M -0.7% Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5d9f1ccb6d |
feat(host): --no-mdns/PUNKTFUNK_MDNS gate + punktfunk1-host --pairing-pin — docker-testing enablers
ci / docs-site (push) Successful in 54s
ci / web (push) Successful in 57s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
apple / swift (push) Successful in 1m6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
decky / build-publish (push) Successful in 24s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 46s
ci / bench (push) Successful in 6m13s
apple / screenshots (push) Successful in 5m33s
windows-host / package (push) Successful in 9m42s
docker / deploy-docs (push) Successful in 5m59s
android / android (push) Successful in 13m48s
arch / build-publish (push) Successful in 13m59s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m52s
deb / build-publish (push) Successful in 15m5s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m7s
ci / rust (push) Successful in 18m35s
mDNS advertisement (native _punktfunk._udp AND GameStream _nvstream) can now be disabled with --no-mdns on serve/punktfunk1-host or PUNKTFUNK_MDNS=0 (the PUNKTFUNK_ZEROCOPY off-grammar) — for multicast-dead environments (bridged Docker, CI netns) where the advert reaches nobody or fails outright and aborts the GameStream plane; clients dial a manually-added host instead (mDNS-blind host-add works since the 0.8.4 dial-first fix). Unit tests no longer advertise on the LAN (mdns: false). punktfunk1-host gains --pairing-pin <PIN> (the already-plumbed fixed-PIN test seam, now CLI-exposed; empty refused like --mgmt-token) so a CI harness runs a deterministic SPAKE2 ceremony instead of scraping the logged PIN. Verified live: --no-mdns + PUNKTFUNK_MDNS=0 skip the advert, --pairing-pin arms with the fixed PIN, and a punktfunk1-host --source synthetic + punktfunk-probe loopback run passes verification (60/60 frames, 0 mismatch). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
838a1239cf |
feat(codec): make client codec selection real — GPU-aware native advertisement
apple / swift (push) Successful in 1m7s
release / apple (push) Successful in 8m27s
windows-host / package (push) Successful in 7m59s
apple / screenshots (push) Failing after 2m38s
android / android (push) Successful in 4m44s
ci / rust (push) Failing after 46s
arch / build-publish (push) Successful in 5m32s
ci / web (push) Successful in 50s
ci / docs-site (push) Successful in 1m2s
deb / build-publish (push) Successful in 4m44s
ci / bench (push) Successful in 5m7s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
decky / build-publish (push) Successful in 13s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 11m9s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 11m14s
docker / deploy-docs (push) Successful in 13s
The clients' codec pickers sent a preference the host threw away: host_wire_caps() hardcoded HEVC for every GPU backend, so the native path never emitted H.264/AV1 regardless of what the user chose. Host: advertise what the backend actually encodes, mirroring the GameStream serverinfo logic — software openh264 emits H.264; probed backends (Linux VAAPI, Windows AMF/QSV) advertise their per-GPU CodecSupport via the new wire_mask(); NVENC keeps the Moonlight-validated H.264|HEVC|AV1 static superset; an empty probe falls back to the superset so auto clients still resolve HEVC. Gate 10-bit to HEVC (like the 4:4:4 gate) now that a client can steer the codec — Main10 is the only 10-bit encode path. Fix the web-console stats label hardcoded to "hevc" (new Codec::label(), shared with the GameStream register_session mapping). Android: replace the hardcoded H264|HEVC Hello advertisement with a videoCodecs param fed by VideoDecoders.decodableCodecBits() (AV1 bit only when a real hardware video/av01 decoder exists — the decode loop was already mime-driven); offer AV1 in the codec picker on capable devices. Decky: add the missing "Video codec" dropdown (auto/hevc/h264/av1) to the plugin settings, round-tripping the same codec key the flatpak client reads. Apple: unchanged by design (AnnexB.swift is NAL-only, AV1 is never advertised); refresh the stale "hosts don't emit AV1 on the native path yet" comments here and host-side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
349d16382e |
fix(stats): decode stage measures GPU completion; Alt+Enter fullscreen alias
The Vulkan path's receive_frame returns at SUBMISSION (~0.1 ms) — the hardware decodes asynchronously, so the decode stat was truthful but measured the wrong boundary. The pump now ships the frame to the presenter FIRST, then waits the frame's timeline fence (vkWaitSemaphores resolved through the shared device's proc chain) and stamps received→decode-COMPLETE — true NVDEC time at zero pipeline cost, since the presenter's own GPU wait is what actually gates sampling. Software/ VAAPI keep their synchronous stamps. Also: Alt+Enter joins F11 as the fullscreen toggle (some keyboards' Fn layer sends a media key for plain F11 — observed on glass as 'F11 only works with shift'); the shell's shortcuts panel lists both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6f47abab8c |
feat(host): native AMF SDK encoder for Windows AMD — drop libavcodec
Direct-SDK AMF encoder (encode/windows/amf.rs), the AMD analogue of the direct-NVENC path, replacing the libavcodec *_amf dispatch. C-vtable FFI pinned to AMF headers v1.4.36, runtime-loaded from the driver's amfrt64.dll (no build feature, no new dependency) exactly as NVENC loads its DLL. - AVC/HEVC (SDR NV12 + 10-bit HDR P010) and AV1 (RDNA3+, probed); a bounded poll retires the libavcodec ~2-frame output hold; native in-place reset(). - Intra-refresh wave (PUNKTFUNK_INTRA_REFRESH), in-band HDR mastering/CLL metadata (*InHDRMetadata -> HEVC SEI / AV1 OBU), and a native codec probe feeding the GameStream advertisement (windows_backend_is_ffmpeg -> windows_backend_is_probed). - AMD dispatch / advertisement / 4:4:4 are native-only; the libavcodec AMF fallback and the PUNKTFUNK_AMF_FFMPEG hatch are removed. FFmpeg serves QSV only (its AMF path retained solely as the latency A/B comparator). - Overload back-pressure: submit bounds in-flight surfaces below the input ring, draining finished AUs (buffered for poll, FIFO-preserved) to free a slot and retry on AMF_INPUT_FULL instead of tearing the encoder down and forcing an IDR; this also closes a latent ring-overwrite corruption seen under load on-glass. Validated on the lab Ryzen iGPU (AMF runtime 1.4.37): HEVC/AVC across a native reset, HEVC Main10 mastering+CLL SEIs byte-verified, intra-refresh accepted, a backpressure burst FIFO-clean, and end-to-end via the macOS client. Measured §5.2 latency A/B: native encode_us p50 ~5 ms (0.31 frame periods) vs libavcodec ~17 ms (1.01). 4:4:4 stays unsupported (VCN hardware limit). Live-gated tests skip cleanly on non-AMD boxes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1992eb1c52 |
feat(host): game-mode integration + dedicated game sessions
Implements design/gamemode-and-dedicated-sessions.md (Parts A1-A5 + B0-B2):
reconciles the merged display-management registry with session-mobile
Bazzite/SteamOS hosts and adds a per-launch dedicated gamescope mode.
- A1 DisplayOwnership {Owned,External,SessionManaged} + poolable_now(): the
registry pools only what it owns, so gamescope managed/attach outputs are no
longer double-owned by the registry AND the gamescope restore worker (fixes
the game-mode-reconnect stale-node wedge).
- A2 validated reuse: (backend,mode,launch,epoch) reuse key + kept_display_alive
liveness probe + reused_gen/mark_failed on a reused-display first-frame failure.
- A3 policy-driven managed restore (keep_alive replaces the hardcoded 5s debounce;
forever = held = gaming-rig truthful) + crash-restore persist + SIGKILL teardown
(kill_unit, applied to our transient unit AND the autologin stop -- validated
live on .181 to avoid the F44 GPU-context leak).
- A4 session epoch: observe_session_instance bumps the epoch + invalidate_backend
on a desktop-compositor instance change; gamescope spawns are exempt.
- A5 per-spawn log + PID-scoped gamescope node discovery.
- B0 game_session {auto,dedicated} policy (top-level, preset-orthogonal) +
pick_gamescope_mode dedicated_launch + steam -silent command shaping.
- B1 free the autologin Steam before a dedicated Steam spawn (single-instance).
- B2 game-exit -> APP_EXITED_CLOSE_CODE (0x52) clean session end.
Adversarially reviewed (11 findings fixed). Validated on glass (.181 Bazzite F44,
RTX 4090): dedicated spawn streams a real game smoothly; keep-alive reuse; the
SIGKILL fix avoids the F44 vkCreateDevice leak. Workspace green
(build / test --workspace / clippy -D warnings / fmt), OpenAPI + C header
regenerated, web console tsc + vite build green. clients/probe: bump the
no-video timeout 8s->45s for gamescope cold starts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
07c8b868e6 |
style(host): cargo fmt --all (rustfmt 1.9.0 drift)
The CI image's rustfmt reformats these files (multi-line assert!/tracing! macros, match-arm and struct-variant wrapping) — pre-existing drift that the Format job caught. Reformat to match. Pure formatting; no logic change. main.rs also gets a blank line before a standalone comment so rustfmt stops mis-indenting it as a trailing-comment continuation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e8531a9eac |
feat(vdisplay): harden keep-alive reconnect — same-client preempt, quit-skips-linger, configurable idle
On-glass testing (Test 2, KWin .116) surfaced that a reconnect within the QUIC idle-timeout window (~8s) lands on a fresh SECOND display instead of reusing the kept one: the old session was still Active (not yet Lingering), so the registry's keep-alive reuse (which only matches Lingering) skipped it and the old session kept streaming to nobody. Three fixes: #3 Same-client reconnect preempt (the real fix): admission::preempt_same_identity() lists a reconnecting client's OWN still-live session(s) (same cert fingerprint); serve_session signals their stop + waits the release grace BEFORE acquiring, so the zombie tears down → its display lingers → the reconnect REUSES it instead of making a second. Implements the "preempts downstream" the admission docs already promised. Independent of the mode_conflict policy; the pure core (same_identity_stops) is unit-tested. #2 Deliberate quit skips linger: a client that deliberately disconnects closes the QUIC connection with QUIT_CLOSE_CODE (0x51, shared in core::quic); the host reads the ApplicationClosed reason and tears the display down immediately (registry release() gained force_immediate → Linger::Immediate; multi-session-safe via the pure lifecycle machine), while a bare disconnect still lingers for reconnect. Threaded via a session quit flag → the DisplayLease. NativeClient::disconnect_quit() + punktfunk-probe --quit drive it; GameStream (Quit App / h_cancel) is a documented follow-up. #1 Configurable disconnect-detection latency: the QUIC control-connection idle timeout (stream_transport, 8s default) is host-tunable via --idle-timeout-ms / PUNKTFUNK_IDLE_TIMEOUT_MS, clamped >=1s with a keep-alive that scales to it so a live session never false-closes. Default unchanged (8s stays load-bearing for the Windows IDD-push reconnect flow). Workspace check + 63 core / 215 host / 47 vdisplay tests green; clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0c2808bb4c |
perf(gamestream): move FEC packetization off the encode loop (3-stage pipeline)
FEC/Reed-Solomon packetization ran inline on the encode loop (~3 ms/frame at 4K), serializing behind encode and capping the GameStream frame rate below what the encoder alone can sustain. Split it into a 3-stage pipeline, each stage on its own thread joined by a depth-2 bounded queue: encode loop → [raw AUs] → packetizer (FEC/RS) → [wire batch] → paced sender - `spawn_packetizer`: turns each `RawFrame`'s access units into wire datagrams via the stateful VideoPacketizer, off the encode loop. Above-normal priority (on the per-frame critical path). Tallies goodput (bytes to the wire) for the stats window. - Backpressure chains up: a slow sender blocks the packetizer, which fills the encode→packetizer queue, which makes the encode loop drop the NEWEST frame — encode itself never waits. - A dropped frame now consumes no client-visible frameIndex (packetization is downstream), so the host re-anchors the reference chain: a drop arms a keyframe on the next iteration (`recover_after_drop`), routed through the same coalesce gate as client IDR requests so a burst of drops (congestion) can't become an IDR storm. - Perf/stats relabeled: `pkt` = AU drain, `send` = enqueue to the pipeline (both should be near-zero now; nonzero = encode being stalled by pipeline backpressure). Goodput read from the packetizer's atomic at the 1 s stats boundary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
43e035de41 |
feat(vdisplay/kwin): group-aware exclusive — never disable a sibling output (Stage 5 §6.1)
The critical latent bug Stage 3 introduced: per-slot output names mean a 2nd exclusive session's other_enabled_outputs() (which disabled 'everything not named Virtual-punktfunk') would black out the 1st session's Virtual-punktfunk-<id> output. Fix: recognise the whole managed group by the shared Virtual-punktfunk prefix — exclusive now disables only NON-managed outputs (bootstrap/physical), never a group sibling. Plus first-slot-wins for the group primary (a_managed_output_is_primary): a later session joins as a secondary monitor of the shared desktop instead of stealing the shell off the first. Unit-tested. Start of Stage 5 (§6A many-clients-one-desktop). Remaining: Mutter/wlroots group-aware analogues, layout (auto-row/manual + /display/layout + console), per-group topology restore, gamescope groups. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a613f16b7d |
fix(vdisplay): Windows admission default is reject, not join (single-capturer limit)
Two concurrent Windows sessions both drive the same pf-vdisplay monitor's single-capturer IDD-push channel (newest-delivery-wins), which freezes the live client and can wedge the driver (observed live: a concurrent-session test wedged .173 → Moonlight 'no video'; needed a reboot). True multi-session capture is §6.6/ Stage 7. So on Windows 'separate' (incl. the unconfigured default) now resolves to REJECT — a 2nd client gets a clean 503 and the live session is protected — instead of join (which would freeze it). join/steal stay explicit opt-ins; Linux keeps separate (real multi-view). Centralized as admission::effective_conflict(), shared by the native handshake + GameStream h_launch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
aa2041c910 |
refactor(gamestream): extract + unit-test gamestream_admission (Stage 4)
Pull the GameStream mode-conflict decision out of h_launch into a pure gamestream_admission(live, req_fp, policy) -> GsDecision so the 503/join/take-over logic is unit-tested (no live session / same-client → Serve; different client → Reject/Join/Serve per policy; anonymous requester treated as different) — the GameStream path can't be driven without a Moonlight client, so this covers the logic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6ddce1049b |
feat(vdisplay): finish Stage 4 — typed reject, Windows join-default, GameStream 503
Completes the mode-conflict admission surface deferred from the initial Stage 4: - REJECT now delivers the reason to the client: punktfunk/1 closes the QUIC connection with a distinct BUSY code (0x42) + the 'host busy: streaming WxH@Hz to <client>' string, which the client reads from ApplicationClosed (validated on loopback: the probe logs 'closed by peer: host busy … (code 66)'). - Windows default: separate (incl. the unconfigured default) resolves to JOIN — the Windows native host admits a second client at the live mode instead of the old silent last-wins reconfigure of the shared monitor (release-note behavior fix; the reconfigure is now opt-in as steal). separate stays multi-view on Linux. - GameStream 503: h_launch tracks the session owner fp (LaunchSession.owner_fp, kept [u8;32] for Copy) and applies the policy when a DIFFERENT paired client launches — reject → 503 (Moonlight 'host busy'), join → serve the live mode, steal/separate → take over. Same-client re-launch is never a conflict. Native reject-reason loopback-validated; Windows join-default pending .173 rebuild; GameStream 503 pending a Moonlight client (can't drive /launch autonomously). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ed0881a9b9 |
feat(vdisplay): Linux keep-alive pool — registry-owned display lifecycle (Stage 1b)
The ownership split (design/display-management.md §3): the registry owns the per-session virtual-display lifecycle on Linux, so a display can outlive its session (keep-alive) and be reused on reconnect. - registry.rs: a Linux pool driven by the pure lifecycle machine. acquire() reuses a kept (lingering/pinned) display of the same backend+mode, else creates one and keeps the backend's keepalive so the compositor output (and its PipeWire node_id) survives the session. The session's capturer holds a gen-stamped DisplayLease instead of the real keepalive; its drop drives linger/teardown. Enabling fact: KWin/Mutter/gamescope put their node on the DEFAULT PipeWire daemon (remote_fd=None) — reconnect re-attaches by node_id, no fd re-open. wlroots (remote_fd=Some, xdpw portal) passes through unchanged (teardown-on-drop) pending the fresh-portal-capture re-attach. - Default (unconfigured) linger = Immediate → today's teardown-on-disconnect, so no behavior change without a keep-alive policy; concurrent sessions still each create their own output (reuse only matches LINGERING entries). - Wired build_pipeline (punktfunk1) + gamestream through registry::acquire; capture_virtual_output signature unchanged. Windows delegates to vd.create (the manager already leases) — unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c33bdeab15 |
fix(windows-host): cross-plane IDD serialization, linger-expiry race, second-host guard
Batch C of the audit's medium tier (M7+M8+M9): - M7: GameStream sessions now run the same begin_idd_setup dance as punktfunk/1 before creating the shared monitor. A GS connect could previously ADD/reconfigure the monitor while a native session was mid-build (and vice versa), and its sealed-channel delivery replaced the native ring (newest-wins) — each plane could freeze the other. GS has no cooperative stop plumbing, so it registers a flag nobody reads: a later session signals it, waits the 3 s grace, then force-preempts — the intended handover. - M8: the linger-expiry teardown now runs UNDER the state lock. Running it outside let a concurrent acquire see Idle and ADD+isolate while the old monitor's pinger-join / CCD-restore / REMOVE were still in flight — a failed or de-isolated session exactly at the expiry boundary. A racing acquire now waits the few teardown seconds instead. Lock order stays state → device everywhere; the pinger takes only the device lock. - M9: a named mutex (Global\punktfunk-vdisplay-manager) makes a SECOND host process fail its vdisplay open loudly instead of firing a startup CLEAR_ALL that razes the live host's monitors mid-stream (the admin footgun the shared watchdog then masked). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
65d49f6430 |
fix(gamestream/tests): sender_delivers_batches flaked under CI load — burst overflowed the default socket buffer
The test burst 3×100 1200 B datagrams into an undrained loopback socket: at ~2.5 KB kernel truesize each, the default ~212 KB rmem holds only ~80, so on a starved CI runner (parallel release builds) the kernel silently dropped the overflow and the recv loop could never reach 300 — surfacing as WouldBlock after the 3 s timeout. Size the burst (3×20) to fit the default buffer even with zero concurrent draining, and give recv a starvation-tolerant 10 s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
092525e65b |
feat(host/gamestream): priority-boost the video + send threads like the native path
The GameStream video thread ran unboosted on Linux and the send thread only got the Windows MMCSS call; both now use boost_thread_priority (Linux nice -10/-5, Windows HIGHEST/ABOVE_NORMAL + session tuning). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
03ad2406b6 |
fix(host/windows): layout-correct keyboard injection - semantic vs positional VKs
First-party punktfunk clients send US-positional VKs (the physical key's US-layout VK), GameStream/Moonlight clients send layout-semantic VKs (Sunshine's model). The SendInput injector previously resolved everything through the SYSTEM service's layout - on a German host that is the y/z swap and u-umlaut-on-o-umlaut scramble. GameStream ingest now tags its key events KEY_FLAG_SEMANTIC_VK (stripped from punktfunk/1 wire events so a network client can't flip the convention); the injector maps semantic VKs under the foreground app's layout and positional VKs through a fixed scancode table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
138a6ed57d |
fix(host): make client game launches work on every Linux compositor
Client-initiated launches (Hello.launch / GameStream applist) were only wired to gamescope's bare-spawn path via the process-global PUNKTFUNK_GAMESCOPE_APP env — which leaked across sessions, was never read by kwin/mutter/wlroots (launch was a silent no-op there), and was unreachable on gamescope anyway because apply_input_env unconditionally defaulted to the managed session (which bails on non-Bazzite/SteamOS boxes and ignores the launch command in all its modes). - Thread the launch per-session: resolve the library id at handshake, carry it on SessionContext (Windows: id; else: resolved command), and hand it to the backend instance via set_launch_command — the global env write is gone (the env stays as an operator fallback in spawn). - Gamescope sub-mode ladder (pick_gamescope_mode, pure + unit-tested): managed only when session-plus/SteamOS infra exists, attach for an explicit request or a foreign (non-host-descendant) gamescope, else bare spawn — which nests the launch and is now reachable on plain distros instead of the guaranteed managed-mode bail. - launch_session_command: one launch entry point for both planes once capture is live — desktop compositors plain-spawn into the retargeted session (the virtual output is primary); managed/attached gamescope spawns with the live session's DISPLAY/GAMESCOPE_WAYLAND_DISPLAY discovered from /proc (steam:// URIs also forward over Steam's own pipe). launch_is_nested gates bare spawn against double-launching. - GameStream unified onto the same dispatch; also nests library-id picks into gamescope (previously only apps.json cmd was nested). Validated live on the dev box up to the missing-GPU wall: handshake resolution, Spawn sub-mode on plain Ubuntu, gamescope spawned with the command nested. On-glass validation (kwin spawn on the streamed output, Bazzite/Deck managed forward) pending GPU reattach. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ae51276a03 |
feat(web): consolidate paired devices, self-contained sections, docs + lint
Web console - Pairing/Library/Stats refactored into self-contained subsections that each own their own queries + mutations; a shared slot-based layout (view.tsx) is filled by the live page (containers) and Storybook (pure cards + fixtures) so the layout can't drift. - All paired devices in one list on Pairing with a protocol column (punktfunk/1 + Moonlight), routing each unpair to the right endpoint; the redundant Clients page is removed. - Library: overview grid split from the add/edit form into separate files. - Login screen links out to the docs. Docs - "Console login password" section on every host page (apt/RPM/Bazzite/SteamOS/Windows) plus a new "Forgot your Password?" troubleshooting page, linked from the login screen. - Console served as HTTP/1.1 over TLS (drop the unusable HTTP/3 advertising) across the Bun entry, launchers, systemd units, and packaging. Tooling - Biome now respects .gitignore (stops linting generated code), config migrated to 2.5.1; all lint issues fixed cleanly. Also includes this branch's in-progress host, Apple client, packaging, and CI changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
ce060d5c49 |
feat(gamestream): advertise HDR + surface the game library (with covers) to Moonlight
Bring the GameStream/Moonlight plane up to the native plane's capability parity. HDR (Windows only): - New host_hdr_capable() gate (Windows + PUNKTFUNK_10BIT, matching the native policy). serverinfo layers SCM_HEVC_MAIN10 onto the probed/static codec mask, so Moonlight finally offers its HDR toggle (live: mask 0x10101 -> 0x10301). - Parse the client's dynamicRangeMode into StreamConfig.hdr and pass it through to OutputFormat::resolve, so a client HDR request proactively enables advanced color on the per-session virtual display (PQ flows even from an SDR desktop). The encoder bit depth now derives from the captured frame format (gs_bit_depth) rather than a hard-coded 8 that mislabeled the already-Main10 HDR stream. Game library in /applist: - The catalog now layers library::all_games() (Steam/Epic/GOG/Xbox/custom) on top of Desktop/apps.json, each with a STABLE GameStream id (FNV-1a, dedup-probed) and the store-qualified library id. Launch routes through the existing security-reviewed launch_title/launch_command via library::launch_gamestream_library — a client can only pick an existing title, never inject a command. - /appasset cover proxy: Moonlight fetches per-app covers from the host, so resolve appid -> library cover URL and proxy the bytes (portrait -> header -> hero -> logo; data: + bounded http(s) fetch), on a blocking thread. IsHdrSupported reflects the host HDR capability. 4:4:4 stays off on GameStream by design: stock Moonlight is 4:2:0 and the Windows IDD-push capturer can't deliver full chroma yet (capturer_supports_444() == false); the gate is documented so it lights up once IDD-push full-chroma capture lands. Validated live (Moonlight -> Windows NVENC host): HDR advertised, the Epic library shows with covers, launch works. clippy clean; apps/serverinfo/library unit tests cover the HDR mask, stable-id, dedup, and data-URL paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
0ff4373abd |
fix(windows/gamestream): create the virtual display on Windows (Moonlight black screen)
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
windows-host / package (push) Has been cancelled
flatpak / build-publish (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 / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (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 / deploy-docs (push) Has been cancelled
decky / build-publish (push) Has been cancelled
deb / build-publish (push) Has been cancelled
android / android (push) Has been cancelled
web-screenshots / screenshots (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
release / apple (push) Has been cancelled
linux-client-screenshots / screenshots (push) Has been cancelled
android-screenshots / screenshots (push) Has been cancelled
The GameStream video path (open_gs_virtual_source) ran the Linux compositor-
detection state machine on every platform. On Windows detect_active_session()
returns None and vdisplay::detect() bails ("could not detect compositor ...
XDG_CURRENT_DESKTOP=''"), killing the video thread right after RTSP PLAY — so a
Moonlight client paired, negotiated, then black-screened and dropped.
The native punktfunk/1 path already guards this (resolve_compositor returns a
placeholder Compositor on Windows, since vdisplay::open ignores the compositor
arg there and always uses the pf-vdisplay IddCx backend). Mirror that guard in
the GameStream path: short-circuit to a placeholder on Windows, keep the Linux
session detection (apply_session_env/apply_input_env) under cfg(not(windows)).
Validated live: Moonlight -> this box now creates the pf-vdisplay virtual
monitor, attaches the IDD-push ring, and NVENC streams 5120x1440@240.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
7cc2ed85cb |
feat(proto/steam): M3 — rich Steam wire (back buttons + 2nd trackpad)
Carry the rich Steam Controller / Steam Deck inputs end-to-end on the wire — strictly additive + forward-compatible (unknown kinds/bits drop on old peers). Core (punktfunk-core): - input.rs: BTN_PADDLE1..4 + BTN_MISC1 in Moonlight's buttonFlags2<<16 namespace (so the GameStream paddle path and native grips share one host injector map; Steam L4/L5/R4/R5 reuse the four Xbox-Elite paddle slots). - quic.rs: RichInput::TouchpadEx (kind 0x03 — surface 0/1/2, touch+click, signed coords, pressure; the second trackpad the single Touchpad can't express) and HidOutput::TrackpadHaptic (kind 0x04 — the SC voice-coil pulse). Round-tripped. - abi.rs: PUNKTFUNK_GAMEPAD_STEAMDECK=6 / _STEAMCONTROLLER=5, the paddle bits, RICH_TOUCHPAD_EX / HIDOUT_TRACKPAD_HAPTIC constants. from_hid packs TrackpadHaptic into the existing which + effect[0..6] — the legacy structs do NOT grow (guarded by new size_of==20/19 asserts); GamepadPref lockstep + paddle-bit lockstep asserts extended. include/punktfunk_core.h regenerated. Host (punktfunk-host): - steam_proto::from_gamepad maps the wire paddles -> the four Deck grips + QAM; apply_rich routes TouchpadEx left/right -> the matching pad. - every DualSense/DS4 manager (Linux + Windows) gained a TouchpadEx arm (surface 0/2 -> its one touchpad; surface 1 ignored) so the variant compiles everywhere and a Steam client streaming to a DS host keeps its right pad. - the xpad BUTTON_MAP finally consumes the GameStream paddle bits (BTN_TRIGGER_HAPPY5-8) — Sunshine/Moonlight paddle clients were silently no-op'd before (design §5.6). - Android feedback: drop TrackpadHaptic (no coils; rumble rides 0xCA). Validated on-box: the ignored backend test now drives the full wire path — from_gamepad (BTN_A + the L4 grip) + apply_rich (a left-pad TouchpadEx) reach the evdev as BTN_A + ABS_HAT0X=-8000. Wire round-trips + paddle/TouchpadEx mapping unit-tested. Workspace clippy/fmt/test green. Not pushed. Deferred to M4: the C-ABI PunktfunkRichInputEx + send_rich_input2 (only the Apple/embedder *send* path needs it; the host decodes TouchpadEx today). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c0e4a6e690 |
fix(host/gamestream): correct the rsa-Marvin (S7) rationale + cap pairing signatures
Red-team found the .cargo/audit.toml justification for RUSTSEC-2023-0071 was materially wrong: it claimed "Marvin targets decryption, so the vulnerable path isn't exercised" — but the advisory is a variable-time modexp of the secret exponent, which RSA *signing* (signing_key.sign) also runs. The accept is still correct, for the RIGHT reasons (no decryption/padding oracle; the signed serversecret is host-random not attacker-chosen; signing is operator-PIN-gated; GameStream is off by default and the native QUIC plane uses rustls, not rsa; Moonlight mandates RSA-2048 so the GameStream key can't move off it). Rewrite the rationale accordingly. Also shut the timing-sample amplifier the review surfaced: the pairing session was never marked after phase 3, so a peer past phase 1 could loop phase2/phase3 to harvest many RSA signing-time samples. Sign exactly once per ceremony (reject a repeated serverchallengeresp). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
59e04aec99 |
chore(deps): drop unmaintained rustls-pemfile; axum-server 0.7 -> 0.8
axum-server was used only for the plain-HTTP nvhttp listener, but we enabled its tls-rustls feature (HTTPS is hand-rolled over tokio-rustls) — and that feature was what pulled the unmaintained rustls-pemfile (RUSTSEC-2025-0134). Drop the feature, bump axum-server to 0.8 (0.8 also no longer pulls it), and move our own PEM parsing in gamestream/tls.rs to rustls-pki-types' PemObject (the same path punktfunk-core/quic.rs already uses), removing our direct rustls-pemfile dep too. Net: rustls-pemfile fully gone; dependency graph trimmed 547 -> 529 crates (the tls-rustls feature also dragged in prettyplease + a wasm-tooling chain). cargo audit now reports only audiopus_sys + paste (transitive, latest, no successor). 108 host tests + clippy + fmt green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c1ce231dbf |
fix(host/security): Windows DACL hardening — close audit #2, #3, #8, #11
Windows local-privilege findings from design/security-review-2026-06-28.md. These are #[cfg(windows)] paths (verify in CI / on the box; this Linux dev VM can't compile MSVC). They follow the existing write_secret_file/icacls patterns; the cross-platform parts are cargo check/clippy/test green. - #2 [HIGH]: route the mgmt bearer token write through the shared write_secret_file so it gets the SAME Windows DACL (SYSTEM/Administrators) as the host key — it was cfg(unix)-only and left Users-readable, leaking full mgmt admin authority to any local user. - #3 [HIGH]: create_private_dir now applies a restrictive DACL to the %ProgramData%\punktfunk config directory (re-owns to Administrators to defeat a pre-creation, strips inheritance, SYSTEM/Admins/OWNER full + Users read-only) so a local user can't plant host.env/apps.json that the SYSTEM service trusts (env/arg-injection LPE). host.env is now written DACL-locked via write_secret_file; the config + logs dirs go through create_private_dir. - #8 [LOW]: write the web-console password file empty, icacls-lock it, THEN write the secret — closes the brief write-then-icacls TOCTOU window. - #11 [LOW]: the SYSTEM logs dir is DACL-locked (Users read-only, no create), so a local user can't pre-plant host.log as a reparse/hardlink to redirect SYSTEM's writes (subsumed by the #3 dir lockdown). Deferred: #5 (host<->UMDF gamepad/IDD shared-section Everyone:GENERIC_ALL). The section SDDL is intentionally permissive because the UMDF driver opens it under a restricted token of unknown SID/integrity; scoping it blind would likely break the live-validated gamepad/IDD pipeline, so it needs on-box validation first. Tracked in the report. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
46b82d4eee |
fix(host/security): close audit findings S1,#1,#4,#10,#12,#7,#6,S2-S6 (Linux/cross-platform)
Remediations from design/security-review-2026-06-28.md verified on Linux (cargo check/clippy/test green; Windows-gated paths verify in CI): - S1 [HIGH]: bump quinn-proto 0.11.14 -> 0.11.15 (RUSTSEC-2026-0185, pre-auth out-of-order STREAM reassembly memory exhaustion on the always-on default QUIC listener). - #1 [HIGH]: remove the unauthenticated nvhttp `GET /pin` endpoint; the GameStream PIN is delivered ONLY via the bearer-gated mgmt API, so a network client can no longer submit its own displayed PIN and self-pair. - #4 [HIGH->MED]: gate the unauthenticated RTSP/UDP media plane on a paired `/launch` and bind it to the launching client's source IP (threaded through the HTTPS handler), so an unpaired peer can neither start capture on an idle host nor ride a paired client's active launch. - #12: bound concurrent parked pairing waiters (MAX_PARKED_WAITERS) so a pre-auth peer can't pin unbounded 300s handshakes. +regression test. - #10: throttle the per-packet ENet control GCM-decrypt-failed warn (exponential backoff) so a junk flood can't spam the log. - #7 [MED->LOW]: serialize all process-global env mutation on the session-setup path under a new vdisplay::ENV_LOCK (apply_session_env / apply_input_env / the launch-cmd set_var / the gamescope env read), so concurrent native sessions can't race set_var/getenv (data-race UB -> host-wide DoS). Full per-session SessionContext threading remains a follow-up for cross-session value confusion. - #6 [MED]: move the gamescope EIS socket relay from world-writable /tmp to $XDG_RUNTIME_DIR (per-user 0700) and reject a symlinked relay file, so a local user can't intercept (keylog) or deny the remote session's input. - S2: a malformed client Opus mic frame now drops that frame instead of tearing down the shared host-lifetime virtual mic (cross-session DoS). - S3: track held buttons/keys in capped HashSets (was unbounded Vec with O(n) scans) so a paired client can't grow per-session input state. - S5: reject fps==0/absurd at the open_video chokepoint (covers Hello, ANNOUNCE, Reconfigure) so the encoder time_base/pts math can't div-by-0. - S6: bound the shared mic mpsc (drop-newest when full). - S4: cap Epic launcher-cache reads (catcache.bin/.item) so a planted giant can't OOM the host during library enumeration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7612238a59 |
feat(audio): end-to-end 5.1/7.1 surround across the native path + all clients
Adds negotiated 5.1/7.1 surround to the punktfunk/1 protocol and every client (previously stereo-only): - core: new shared `audio` layout table (LAYOUT_51/71 + identity multistream mapping, canonical wire order FL FR FC LFE RL RR SL SR); Hello/Welcome `audio_channels` negotiation via the trailing-byte back-compat pattern (old peers fall back to stereo); C-ABI `punktfunk_connect_ex6`, `punktfunk_connection_audio_channels`, and in-core multistream decode `punktfunk_connection_next_audio_pcm` for embedders without a multistream Opus decoder. Real-libopus channel-identity round-trip test. - host: native audio thread captures + Opus-(multi)stream-encodes at the negotiated count (with a cross-session cached-capturer channel-mismatch fix); GameStream surround unified onto the safe `opus::MSEncoder`, dropping `audiopus_sys` (~4 unsafe blocks) and un-gating Windows GameStream surround; WASAPI loopback capture relaxed to 2/6/8 with the correct dwChannelMask. - clients: Linux (PipeWire), Windows (WASAPI), Android (AAudio) decode via `opus::MSDecoder` + render multichannel; Apple decodes in-core to PCM → AVAudioEngine with an explicit wire-order channel layout; each gains a Stereo/5.1/7.1 setting. `punktfunk-probe --audio-channels N` is the headless validator. Verified on Linux: core/host/linux/probe test suites + the Android Rust (cargo-ndk) build, clippy -D warnings, and rustfmt all green. Windows/Apple builds, all on-glass checks, and the live native loopback are pending (CI / a free box). Also lands the concurrent in-tree HEVC 4:4:4 host work (PUNKTFUNK_444): it shares the same touched files (quic.rs, punktfunk1.rs, encode/*, ...) and so cannot be committed separately from the surround changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6409bdb062 |
feat(host/gamestream): follow Desktop<->Game session switches
The GameStream/Moonlight video plane is a separate encode loop that lacked the
session-following the native punktfunk/1 plane has, so a mid-stream Desktop<->Game
switch killed the stream ("video stream failed") instead of following it.
* Normalize the session env like the native plane: extract open_gs_virtual_source,
which detects the LIVE compositor + apply_session_env/apply_input_env (gamescope
ATTACH default -> resize-on-attach to the box's own game-mode session at the
client mode; KWin/Mutter retargeting). GameStream previously ran a bare detect()
against raw process env, so in game mode it bare-spawned a COMPETING gamescope
instead of attaching to the box's session.
* In-place capture-loss rebuild: replace the `?` that ended the stream with a
bounded rebuild (re-detect the live compositor via the same factory, build the
new source BEFORE dropping the old, reopen the encoder, force an IDR) — keeping
the send thread + packetizer + socket + RTP clock. A same-resolution
Desktop<->Game toggle is now FOLLOWED with no Moonlight reconnect.
Protocol limit (unchanged): a mid-stream RESOLUTION change is impossible on
GameStream (WxH locked at ANNOUNCE; no Reconfigure) — a session toggle keeps the
negotiated mode, so this isn't hit. The portal/synthetic source passes no rebuild
closure (propagates as before).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
da3f37317b |
feat(host): web-console performance capture — record stream stats, graph them
Arm streaming-perf-stats capture from the web console, play, stop, and review the run as graphs; finished captures are saved to disk as browsable/exportable recordings. Covers both the native punktfunk/1 path and GameStream. - stats_recorder.rs: one shared Arc<StatsRecorder> ring (created in gamestream::serve, shared with the mgmt API + both streaming loops, mirroring NativePairing). The hot-path gate is a runtime AtomicBool that replaces the startup-only PUNKTFUNK_PERF for *recording* (PERF stdout logging unchanged); bounded ring (~3 h); atomic temp+rename writes to ~/.config/punktfunk/captures/*.json; path-traversal-safe ids; poison-resilient locks. - native (punktfunk1.rs) + GameStream (stream.rs) emit a StatsSample at their existing ~2 s / ~1 s aggregation boundary — per-stage latency p50/p99, fps new/repeat, goodput, loss/FEC deltas — with no new per-frame work beyond the cheap atomic check. FrameMsg.was_measured keeps pre-arm in-flight frames out of the first window's percentiles (without zeroing the Windows-relay path's fps/encode). - mgmt.rs: 7 bearer-only /api/v1/stats/* endpoints (capture start/stop/status/live; recordings list/get/delete); api/openapi.json regenerated, in sync. - web: new "Performance" page (recharts, rendered SSR-safe) — capture control, live graphs while armed, recordings table (view / download-JSON / delete), and a detail view with the latency stacked-area bottleneck breakdown (p50/p99 toggle) + throughput + health. Charts adapt to either path's stage set. Design: design/stats-capture-plan.md. Built and adversarially reviewed via a multi-agent workflow; workspace build/clippy(-D warnings)/fmt/tests green, OpenAPI no-drift. Not yet on-glass validated against a live session. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0925f1aaa1 |
fix: complete the docs/→design/ and openapi→api/ rename references
The file moves (docs/ → design/, docs/api/openapi.json → api/openapi.json) landed
in
|
||
|
|
79c8641c3f |
docs(host): prove unsafe blocks in the Windows + cross-platform files + gate them (unsafe-proof program 3/N)
Continues the unsafe-proof program across the Windows/cross-platform host files
(~75 blocks, 21 files), each with a SAFETY proof of the real invariant and a
per-file #![deny(clippy::undocumented_unsafe_blocks)] gate:
capture/windows: dxgi.rs, wgc_relay.rs, wgc.rs, desktop_watch.rs, composed_flip.rs
(windows-rs COM: interface validity, same-D3D11-device textures,
immediate-context single-thread, borrowed args outlive the call)
windows: service.rs (SCM/token/CreateProcessAsUserW/event handles — OwnedHandle
liveness, no double-close/signal race), win_display, wgc_helper, interactive
vdisplay/windows: manager.rs, pf_vdisplay.rs (SwDeviceCreate/IddCx/ioctl handle
liveness via the OnceLock VDM singleton + OwnedHandle)
encode/windows: ffmpeg_win.rs (full AVBufferRef refcount audit — balanced, NO leaks,
unlike the vaapi sibling), sw.rs
cross-platform: gamestream/audio.rs (libopus), gamestream/stream.rs (sendmmsg),
inject/windows/sendinput.rs, audio/windows/wasapi_mic.rs,
session_tuning.rs, vdisplay.rs
Two findings (handled separately):
- wgc_relay.rs `unsafe impl Sync for HelperRelay` is UNSOUND (its mpsc Receiver is
!Sync) though not live-exploited — marked SUSPECT inline; fix pending box check
(it touches the in-flight punktfunk1.rs).
- capture.rs / encode.rs (PARENT modules of the WIP idd_push.rs / nvenc.rs) do NOT
get the file deny yet — it would propagate the lint into the undocumented WIP
children. The deny lands there once those are documented (after the WIP commits).
Linux-visible parts verified green (cargo clippy -p punktfunk-host --all-targets
-- -D warnings). The cfg(windows) deny gates are box-verified next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
6062387d2c |
feat(gamestream): launch apps on Windows + Linux non-gamescope hosts
GameStream's apps.json `cmd` is delivered via set_launch_command, which ONLY the Linux gamescope backend nests. On Windows (no gamescope) and Linux kwin/mutter/wlroots (which stream the existing desktop) the command was silently dropped. Now, after capture is live, stream.rs spawns it via library::launch_gamestream_command for those backends — Windows: into the interactive USER session (spawn_in_active_session, since the host is SYSTEM); Linux: a plain `sh -c` spawn into the host's own graphical session so the app lands on the streamed (primary) output. Linux gamescope keeps nesting via set_launch_command and is skipped here to avoid a double launch. The command is operator-typed apps.json (trusted), never client-set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
85973cae43 |
feat(windows-host): EncoderCaps — query RFI/HDR-SEI caps (Goal-1 stage 5, tightening 3)
The last §2.3 seam-trait tightening: give `Encoder` a `caps() -> EncoderCaps`
so the session glue routes by *query* instead of relying on the no-op/`false`
defaults of `invalidate_ref_frames`/`set_hdr_meta`.
`EncoderCaps { supports_rfi, supports_hdr_metadata }` is a cheap `Copy` struct.
The trait gains a default `caps()` returning `EncoderCaps::default()` (all
false) — correct for every SDR/libavcodec backend (Linux NVENC, VAAPI, AMF/QSV,
software openh264), so they need no change. Only the Windows direct-NVENC path
(`NvencD3d11Encoder`) overrides it, reporting the real `rfi_supported` (probed
once at open via `nvEncGetEncodeCaps`) and `hdr` (HDR-SEI on keyframes).
Consumer: the GameStream encode loop (`gamestream/stream.rs`) hoists
`supports_rfi` once before the loop and gates the loss-recovery path on it —
`!(supports_rfi && enc.invalidate_ref_frames(..))` forces a keyframe directly
on non-RFI encoders instead of making an always-`false` call every loss event.
Behaviour-preserving (same keyframe/RFI outcome), one fewer no-op call, intent
explicit. The native host (punktfunk1) uses FEC+keyframes, no RFI consumer.
Linux `cargo clippy -p punktfunk-host --all-targets -D warnings` clean; the
three edited files are rustfmt-clean. The NVENC override is Windows-only
(1:1 with the existing impl style) → CI/on-glass gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
d32e161070 |
feat(windows-host): OutputFormat into the capturer — kill the dxgi back-reference (Goal-1 stage 5, tightening 1)
The headline §2.3 seam tightening (the explicit Stage-3 deferral; §5's "highest-severity
coupling"): the capturer is now TOLD its output format instead of re-deriving the encode backend.
New `capture::OutputFormat { gpu, hdr }`, resolved once per session and passed INTO
capture_virtual_output:
* native punktfunk/1 path: `SessionPlan::output_format()` (gpu = encoder.is_gpu(), from the
already-resolved plan.encoder — no second probe; hdr = plan.hdr).
* GameStream + spike paths: `OutputFormat::resolve(hdr)` (gpu from the single `gpu_encode()`
source, which maps windows_resolved_backend()).
`capture/dxgi.rs DuplCapturer::open` takes `gpu` in and its internal
`!matches!(windows_resolved_backend(), Software)` recompute is DELETED — the capture layer no
longer re-calls the encode layer (the back-reference that could let capture and encode disagree
on whether frames are GPU-resident, plan §2.3/§5). The relay's secure-desktop DDA passes
`gpu_encode()` likewise.
Behavior-preserving: the `gpu` passed in equals the value the capturer used to compute (same
encode-backend resolution). The DDA opens keep `want_hdr=false` (the SDR fallback, unchanged).
Tightenings 2 (HDR/release -> VirtualLease) and 3 (EncoderCaps) split off: (2) needs the
monitor-generation carried on the lease + the keepalive becoming Box<dyn VirtualLease> — that's
the §2.5 ownership-model change (CURRENT_MON_GEN / sudovda::wait_for_monitor_released), so it
moves there; (3) is a small additive follow-on. Documented in the plan.
Verified: Linux cargo check + clippy (-D warnings) + fmt clean. Box build to follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
d40f96c3da |
feat(windows-host): SessionPlan — resolve capture/topology/encoder once per session (Goal-1 stage 3)
New src/session_plan.rs: a Copy `SessionPlan { capture, topology, encoder, bit_depth, hdr }`
resolved ONCE from HostConfig (+ the negotiated bit_depth) at the top of `virtual_stream`,
logged, and threaded through build_pipeline_with_retry/build_pipeline. The three scattered
Windows dispatch points now read this one typed artifact instead of re-deriving from config
(plan §2.4, the "capture and encode disagree on the backend" hazard):
* capture: capture::capture_virtual_output takes a CaptureBackend IN (was re-reading
config().idd_push / capture_backend / no_wgc internally). CaptureBackend::resolve() is the
single resolver, shared with the GameStream + spike call sites.
* topology: virtual_stream reads plan.topology; should_use_helper is deleted (its body is
session_plan::resolve_topology, verbatim). The IDD-push reconnect-preempt guard reads
plan.capture too.
* encoder: recorded as EncoderBackend from encode::windows_resolved_backend (config-backed +
GPU-vendor cached since stage 2 -> already a single source). Threading encoder/input_format
into the encoder+capturer opens (which removes the capture->windows_resolved_backend()
back-reference recomputed in dxgi.rs) is stage 5.
Behavior-preserving by construction: each resolved decision is provably equivalent to the
pre-stage-3 reads (same config() + the same cached running_as_system()/GPU-vendor probes), so
old==new. SessionPlan is platform-neutral so it threads the shared virtual_stream/build_pipeline
signatures; on Linux it resolves to the single portal/single-process path.
Also fixes a pre-existing mod-ordering fmt drift in main.rs (mod config; / mod capture;).
Verified: Linux cargo check + clippy (-D warnings) + fmt clean on the touched files. Box build
(Windows compile) + on-glass (NVENC + IDD-push + mode switch) pending on the RTX box.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
f58c1438d7 |
feat(windows-host): finish HostConfig migration — resolve operator/dispatch knobs once (Goal-1 stage 2)
Migrate 31 genuinely-constant operator/dispatch env::var sites onto HostConfig, so the
capture/topology/encoder decision reads ONE owner instead of being recomputed at each call
site (the latent bug where capture and encode could disagree on the resolved backend, plan §2.4):
idd_push x7, no_wgc, capture_backend, render_adapter, encoder_pref (Linux open_video +
linux_zero_copy_is_vaapi), the Windows vdisplay-backend select, plus the plan-named
secure_dda/idd_depth/zerocopy/ten_bit and the multi-site perf x4 / compositor x5 /
video_source x3 / gamepad. Each HostConfig field's parser is byte-identical to the read it
replaced, so old==new by construction (the plan's "a flipped bool is a silent regression" guard).
Scope correction — the plan's "~64 sites / Linux XDG+compositor included / grep env::var -> 0"
was unsafe as written. Two classes are deliberately KEPT as live reads and documented in config.rs:
* Runtime-mutated session vars. vdisplay::apply_session_env REWRITES the process env on every
connect (the Bazzite Gaming<->Desktop follow): WAYLAND_DISPLAY, XDG_CURRENT_DESKTOP,
XDG_RUNTIME_DIR, DBUS_SESSION_BUS_ADDRESS, and the derived PUNKTFUNK_INPUT_BACKEND,
GAMESCOPE_SESSION/NODE, KWIN/MUTTER_VIRTUAL_PRIMARY, FORCE_SHM. Parsing these once would
freeze them at startup and silently break session-following — they are NOT constant.
* Single-use local tuning with no resolve-once benefit (and FEC_PCT even has two different
semantics): FEC_PCT, VIDEO_DROP, VBV_FRAMES, SPLIT_ENCODE, PACE_BURST_KB, the dxgi timing
knobs, the *_LIVE/test gates, plus path/dynamic reads (config-dir, PATH search,
env-forward-to-child). PUNKTFUNK_ZEROCOPY is split on purpose: Windows presence-semantics
moved to the field; Linux keeps its own truthy (1|true|yes|on) parser.
Verified: Linux cargo check + clippy (-D warnings) + fmt clean on the touched files. The
Windows-only edits are 1:1 substitutions; they get a real Windows compile on the box with Stage 3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
c7ef0e411a |
feat(windows): pf-vdisplay IDD-push — HDR + pipelined zero-copy capture
HDR (display-driven, matching the WGC path): - CTA-861.3 HDR EDID (BT.2020 primaries + HDR Static Metadata block) so Windows offers "Use HDR" on the virtual display. The host FOLLOWS the display's live advanced-color state, recreating the shared ring at the matching format (FP16 in HDR / BGRA in SDR) on a toggle — no freeze. - Always emit Main10/BT.2020-PQ Rgb10a2 while the display is HDR; the client auto-detects PQ from the HEVC VUI (clients under-report VIDEO_CAP_10BIT). Generic HDR10 mastering SEI on every IDR. - Generation-tagged `latest` (gen<<40|seq<<8|slot) + driver `is_stale` re-attach kill the toggle-time garbage frame and any stale-ring read. Perf: - Pipeline the encode loop (Capturer::pipeline_depth; IDD-push = 2): submit N+1 before polling N so the convert/copy on the 3D engine overlaps the NVENC encode of N on the ASIC. PUNKTFUNK_IDD_DEPTH overrides (1 = synchronous). - Rotating host output ring (OUT_RING) so the in-flight encode and the next convert never touch the same texture. - HDR converts directly from the keyed-mutex slot's SRV into the output ring (drops the redundant slot->fp16 scratch copy); SDR copies the BGRA slot in. The slot mutex is held only across the convert/copy, not the encode. RING_LEN 3->6 for publish headroom. - Capture-health diagnostic: new_fps vs repeat_fps under PUNKTFUNK_PERF (a low new_fps at a high send rate means the source isn't compositing, not an encode stall). Validated live on the RTX box: 5120x1440@240 HDR streams; driver composes ~180 new fps, encode 240 fps @ ~4.3 ms p50. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
5852238665 |
feat(windows): AMD (AMF) + Intel (QSV) hardware encode on the Windows host
The Windows host was NVIDIA-only (NVENC) with an openh264 software fallback. Add
AMD AMF and Intel QSV via libavcodec — the Windows analogue of the Linux VAAPI
backend — so one installer serves all three GPU vendors.
- encode/ffmpeg_win.rs: new WinVendor{Amf,Qsv} encoder. System-memory NV12/P010
readback (default, robust) + opt-in zero-copy D3D11 (PUNKTFUNK_ZEROCOPY: shares
the capturer's ID3D11Device; AMF takes AV_PIX_FMT_D3D11, QSV derives a QSV frames
ctx and maps) with a system fallback for the format-group mismatch the capturer's
video-processor fallback can produce. HDR Main10 (P010 + BT.2020/PQ VUI; an
Rgb10a2->P010 swscale covers the shader fallback).
- encode.rs: Codec::amf_name/qsv_name; open_video + windows_resolved_backend()
resolve PUNKTFUNK_ENCODER=auto|nvenc|amf|qsv|sw via a DXGI adapter VendorId probe.
- capture/dxgi.rs: gpu_mode mirrors the resolved backend (D3D11 NV12/P010 for AMF/QSV).
- gamestream/serverinfo.rs: GPU-aware codec advertisement (windows_codec_support;
AV1 gated to RDNA3+/Arc, like the VAAPI path).
- Cargo.toml: amf-qsv feature (optional ffmpeg-next in the windows target block).
- CI/installer: windows-host.yml sets FFMPEG_DIR + builds --features nvenc,amf-qsv;
the Inno installer bundles the FFmpeg DLLs; host.env default nvenc -> auto.
CI-green target; AMF/QSV not yet on-glass validated (no AMD/Intel Windows box in the
lab) — NVENC stays live-validated. An adversarial-review pass caught + fixed real
FFI bugs (AV_PIX_FMT_P010 is a macro -> P010LE; windows-rs 0.62 GetImmediateContext/
GetDesc1 return Result; AV_HWFRAME_MAP_* is a bindgen enum with no BitOr).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
0de648f106 |
feat(host): GameStream/Moonlight compat is now opt-in (--gamestream) — secure native-only by default
Follows the security audit (#5/#9): the GameStream-compat plane carries inherent on-path weaknesses that can't be fixed on the wire without breaking stock Moonlight — its pairing runs over plain HTTP (#9, MITM-able during the pairing window) and its legacy control encryption can reuse GCM nonces (#5, a passive eavesdropper can recover/forge input). The native punktfunk/1 plane (SPAKE2 PIN pairing + per-direction AEAD nonces) has neither. So flip the default to secure-by-default: - `serve` → native punktfunk/1 plane + management API ONLY (no GameStream surface). - `serve --gamestream` → ALSO the GameStream/Moonlight-compat planes (nvhttp pairing, RTSP, ENet control, _nvstream mDNS). Opt-in, logged with a trusted-LAN caveat. `--moonlight` is an alias. - The native plane is now ALWAYS on in `serve` (`--native` is a kept-for-compat no-op); the unified GameStream+native host is `serve --gamestream`. `gamestream::serve` gates the GameStream spawns (nvhttp/rtsp/control/mdns) on the flag; the native plane + mgmt + native-pairing handle always run. To avoid silently regressing validated Moonlight deployments, the explicit deployment configs PRESERVE Moonlight via `--gamestream` (each documents dropping it for a secure native-only host): the Linux systemd unit, the Steam Deck installer, and the Windows service default (DEFAULT_HOST_CMD). The bare `serve` default (new/manual use) is secure. Docs swept to match (host-cli, moonlight, quickstart, install, packaging READMEs, CLAUDE.md, README, …): Moonlight setup now instructs `--gamestream`; native/console refs use bare `serve`. OpenAPI regenerated (a stale "run `serve --native`" string). fmt + clippy clean; 94 host tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ed7dc3ed24 |
fix(security): remaining audit findings — mgmt admin gate, RTSP DoS bounds, FEC drop, ALPN, ct-compare
Addresses the lower-severity findings from docs/security-review.md (#4-#12). Each fix was adversarially re-reviewed (5-agent pass); two review catches folded in (the Apple client's GET /library cert path; an RTSP header-cap bypass + a spawn-panic counter leak). - #4 [low] mgmt mTLS-paired-cert no longer grants full admin. A paired STREAMING cert authorizes only a read-only allowlist (GET /host,/compositors,/status,/clients,/native/clients,/library); every state-changing route and every PIN-exposing route (/pair, /native/pair) requires the operator's bearer token. New cert_auth_is_a_read_only_allowlist test. (/library kept on the allowlist — the native clients browse it cert-only; its mutations stay token-only.) - #6 [low] RTSP pre-auth DoS bounds: a concurrent-connection cap (RAII slot guard), a per-read timeout (slow-loris), and Content-Length/header/message size caps — closing an unauthenticated slow-loris / memory-growth / thread-exhaustion vector on TCP 48010. - #11 [info] A FEC reconstruction failure is now a counted drop (discard the block, keep the session) instead of being stream-fatal — a lossy link can't be torn down by one bad block. - #10 [info] Fixed ALPN ("pkf1") on both native QUIC endpoints (defense-in-depth; a deliberate coordinated client+host upgrade — a new host rejects an ALPN-less old client). - #8 [info] Constant-time GameStream pairing phase-4 hash compare (crypto::ct_eq). - #7 [low] New VirtualDisplay::set_launch_command carries the launch command per-session on the GameStream path (no process-global env stomp under concurrent sessions); native path keeps the env under today's single-session model (documented; plumb per-session with concurrent sessions). - #5 [low] Legacy GameStream GCM nonce reuse: documented as inherent to Nvidia's old-style control encryption (Apollo/Moonlight identical; key is client-known) — unfixable on the legacy wire; the real fix is V2 control-encryption negotiation. Code comment at control.rs. - #9 [info] GameStream plain-HTTP pairing: documented (inherent to GFE compat; use punktfunk/1). - #12 [low] Web global NODE_TLS_REJECT_UNAUTHORIZED: fix designed (undici dispatcher scoped to the loopback mgmt fetch) but DEFERRED — needs `bun add undici` in the web build env; reverted to keep the web working. Latent-only (the loopback mgmt fetch is the console's only outbound TLS). fmt + clippy -D warnings clean; 94 host + core tests green; no C-ABI/OpenAPI drift. (The HDR Steps 1-2 client work in the tree is the user's parallel WIP — deliberately NOT included here.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ffae2a31e4 |
feat: HDR Step-0 colour-metadata transport + security-audit hardening
Two strands, entangled in punktfunk1.rs, committed together (one builds-green tree). HDR pipeline Step 0 — glass-to-glass colour-metadata transport (docs/hdr-pipeline-plan.md): - Protocol/ABI: ColorInfo on the Welcome + a 0xCE HdrMeta datagram carry the source colour space + HDR10 static mastering metadata (quic.rs, abi.rs connect_ex5 fixing caps=0). - New platform-independent, unit-tested HDR static-metadata helpers (hdr.rs): chromaticities (1/50000), mastering luminance (0.0001 cd/m2), MaxCLL/MaxFALL in HDR10/ST.2086 units. - Capture/encode hooks (capture.rs, encode.rs set_hdr_meta) + Linux client / probe plumbing. Security-audit hardening — top 3 from docs/security-review.md, each adversarially verified: - #1 [HIGH] Secret file permissions. The host key.pem/cert.pem and both trust stores are now written owner-only: 0600 + dir 0700 on Unix (mirrors mgmt_token), best-effort SYSTEM/Administrators/OWNER-only icacls DACL on Windows (%ProgramData% is Users-readable). Closes a local key-disclosure -> host-impersonation gap. New gamestream::{create_private_dir, write_secret_file} + a 0600 regression test. - #2 [HIGH] Native SPAKE2 PIN is single-use. The PIN is consumed the moment the host sends its key-confirmation (which lets the client test its one guess), before reading the proof, so any completed attempt -- right OR wrong -- disarms the window. A wrong PIN isn't observable host-side (the client aborts before sending its proof), so consuming on first attempt is what delivers the documented "one online guess" instead of an unbounded brute-force of the static 4-digit PIN. Test verifies single-use. - #3 [MEDIUM] RTSP packetSize is bounded ([64,2048] in stream_config) and VideoPacketizer::new uses saturating .max(1), killing a PRE-AUTH div-by-zero/underflow panic of the video thread. Tests for {0,15,16,17} + out-of-range rejection. fmt + clippy -D warnings clean; full workspace test suite green (93 host tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1e0d2e661f |
feat(host): Apollo-backlog hardening — cert gate, NVENC RFI, media QoS, async injector
A pass over the apollo-comparison backlog (re-verified against current code). Lands four items end-to-end plus a Windows-DualSense scoping doc. - #5/#92/#26 — GameStream paired-cert allow-list. tls.rs surfaces the verified peer cert to handlers (serve_https + PeerCertFingerprint, now shared with the mgmt API instead of duplicated); nvhttp gates /launch /resume /applist /cancel on AppState.paired and reports a real PairStatus; save_paired writes atomically (temp+rename). Closes the "mTLS accepts any client cert" hole. + regression test. - #6/#51/#19/#22 — NVENC caps query -> reference-frame invalidation. nvenc.rs query_caps probes nvEncGetEncodeCaps (max dims / 10-bit / custom-VBV / RFI), rejecting over-range modes and degrading 10-bit->8-bit instead of an opaque InvalidParam. New Encoder::invalidate_ref_frames (default false -> caller keyframes); the Windows NVENC path implements real RFI (multi-ref DPB + nvEncInvalidateRefFrames, dedup + IDR-on-overflow). control.rs decodes the 0x0301 lost-frame range (Apollo's IDX_INVALIDATE_REF_FRAMES) -> AppState.rfi_range -> encode loop, falling back to a keyframe. NOTE: the Windows NVENC impl is RTX-box/CI-pending (can't compile on Linux); adversarially reviewed vs the SDK. - #43/#72 — media socket QoS + buffer growth. New punktfunk_core::transport::qos: grow_socket_buffers (factored out the native plane's 32MB SO_SNDBUF growth so the GameStream sockets reuse it) + set_media_qos (opt-in PUNKTFUNK_DSCP=1: DSCP CS5 video / CS6 audio + Linux SO_PRIORITY, Apollo's scheme). Wired into UdpTransport and the GameStream video/audio sockets. Windows IP_TOS needs qWAVE (follow-up). - #8/#45 — GameStream input injection off the ENet service thread. on_receive no longer injects inline (a slow inject head-blocked ENet keepalive/retransmit); it forwards to a dedicated injector thread. The hardened InjectorService moved from punktfunk1 into crate::inject (shared by both planes) + a coalesce step that sums adjacent relative-mouse/scroll deltas while preserving button/key/abs ordering. Docs: re-verified apollo-comparison.md status (22 items already done/obsolete since the snapshot) + windows-dualsense-scoping.md (ViGEm can't emulate a DualSense; real DS5 on Windows needs a VHF virtual-HID driver — web-research pass pending). fmt + clippy -D warnings clean; full workspace test suite green; no C-ABI/OpenAPI drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
68e772b662 |
fix(host/serverinfo): don't advertise an empty codec mask when the VAAPI probe finds nothing
The Phase 3 GPU-aware codec mask (
|
||
|
|
e2cef91d3a |
feat(host): VAAPI codec probe + AMD/Intel packaging + neutral logs (Phase 3)
Polish for AMD/Intel support:
- GameStream serverinfo advertises only codecs the GPU can ACTUALLY encode on
the VAAPI backend (probed once by opening a tiny encoder per codec). AV1
encode is narrow (Intel Arc/Xe2+, AMD RDNA3+/RDNA4) and an old iGPU may lack
HEVC, so a Moonlight client never negotiates a codec the encoder can't open.
NVENC/Windows keep the Moonlight-validated static mask. Validated on a Radeon
780M: h264/h265/av1 all probe true -> mask unchanged (65793).
- Packaging: Recommends mesa-va-drivers + intel-media-va-driver (deb) /
mesa-va-drivers + intel-media-driver (rpm) so the auto-selected VAAPI backend
works out of the box on AMD/Intel; NVIDIA boxes can --no-install-recommends.
(Fedora note: stock mesa-va-drivers disables HEVC/AV1 -- needs the freeworld
variant from RPM Fusion.)
- De-NVIDIA-fy the user-facing encoder log/context strings ("open NVENC" ->
"open video encoder") now that VAAPI is a first-class backend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
ea84c83785 |
perf(host): latency hardening for the game-vs-encode GPU contention collapse
Verified, prioritized analysis in docs/host-latency-plan.md (multi-agent investigation + adversarial verification). Lands the two low-risk tiers: Tier 2B — Linux scheduling hygiene: - boost_thread_priority now nices the capture/encode (-10) and send (-5) threads on Linux (setpriority, best-effort; no-op without CAP_SYS_NICE), and the wrong "gamescope caps the game" doc-comment is corrected. - CUDA context created with CU_CTX_SCHED_BLOCKING_SYNC (frees a core on the shared box instead of busy-spinning on completion). - Copies moved off the default stream onto a per-thread highest-priority CUDA stream (cuStreamCreateWithPriority, graceful NULL-stream fallback) with a per-stream sync that no longer blocks on the other worker thread's in-flight copies. Stream priority is measure-then-keep (NVIDIA Linux may ignore it); never regresses. Tier 3A — Windows session tuning (new session_tuning.rs, raw C-ABI FFI, no-op off Windows): once-per-process 1ms timer + DwmEnableMMCSS + HIGH priority class; per-thread MMCSS "Games" + keep-display-awake. Wired into both the native (boost_thread_priority) and GameStream (stream.rs) paths. We had zero session tuning before (Apollo streaming_will_start parity). Tier 2A (Linux NV12 convert) is specified but intentionally not landed: it is colour-correctness-critical and needs A/B validation on a GPU box with a display (green-screen risk). Builds + clippy + fmt green on Linux. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |