Files
punktfunk/crates/punktfunk-core/src/transport/mod.rs
T
enricobuehler 450bcf1e7b 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>
2026-06-21 00:06:30 +00:00

77 lines
4.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Pluggable packet I/O. The hot path calls [`Transport::send`] / [`Transport::recv`]
//! directly — no async runtime is involved.
mod loopback;
mod qos;
mod udp;
pub use loopback::{loopback_pair, LoopbackTransport};
pub use qos::{grow_socket_buffers, set_media_qos, MediaClass};
/// Windows-only: reusable USO (UDP Send Offload) batch send for callers that own their own connected
/// socket (the GameStream video sender) rather than going through [`UdpTransport`].
#[cfg(target_os = "windows")]
pub use udp::send_uso_all;
pub use udp::{spawn_data_punch, UdpTransport, PUNCH_MAGIC};
/// A datagram transport. `recv` is non-blocking: it returns `Ok(None)` when no packet
/// is currently available, so the caller (decode/present thread) never blocks here.
pub trait Transport: Send + Sync {
/// Send one packet. `Ok(true)` = handed to the kernel; `Ok(false)` = dropped locally because
/// the send buffer was momentarily full (WouldBlock) — a non-fatal loss the FEC/keyframe path
/// recovers, surfaced so the caller can count it (`packets_send_dropped`) instead of it being
/// invisible. `Err` = a real send failure.
fn send(&self, packet: &[u8]) -> std::io::Result<bool>;
/// Send a whole frame's packets in as few syscalls as possible, returning how many were
/// handed to the kernel (the caller counts `packets.len() - sent` as send-buffer drops). This
/// is the 1 Gbps+ lever: the [`UdpTransport`](super::UdpTransport) override uses `sendmmsg`
/// (~64 packets/syscall) instead of one `send` each — at ~125k pkt/s that is the difference
/// between ~2k and ~125k syscalls/sec. The default is the scalar `send` loop (correct for the
/// loopback transport and non-Linux builds). On a full send buffer it stops early and reports
/// the partial count rather than blocking — same lossy, FEC-protected contract as `send`.
fn send_batch(&self, packets: &[&[u8]]) -> std::io::Result<usize> {
let mut sent = 0;
for p in packets {
if self.send(p)? {
sent += 1;
}
}
Ok(sent)
}
/// Send a frame's equal-size packets using UDP Generic Segmentation Offload where available:
/// one `sendmsg` hands the kernel a big buffer it splits into `gso_size` UDP datagrams, building
/// ~1 GSO skb per ≤64 segments instead of one skb per packet. This is the multi-Gbps lever —
/// research shows ~2.4× throughput at equal CPU and ~40× fewer syscalls, and that `sendmmsg`
/// batching alone is insufficient (it still builds one skb per datagram). The
/// [`UdpTransport`](super::UdpTransport) Linux override implements it (opt-in via `PUNKTFUNK_GSO`,
/// auto-fallback on any GSO error); the default just delegates to [`send_batch`](Self::send_batch),
/// correct for loopback and non-Linux. Same lossy, FEC-protected short-count contract as `send_batch`.
fn send_gso(&self, packets: &[&[u8]]) -> std::io::Result<usize> {
self.send_batch(packets)
}
fn recv(&self) -> std::io::Result<Option<Vec<u8>>>;
/// Receive up to `out.len()` datagrams in as few syscalls as possible, writing each into its
/// `out[i]` buffer (sized ≥ a max datagram) and its byte count into `lens[i]`; returns how many
/// arrived (`0` = none available; non-blocking). The recv counterpart of [`send_batch`]: the
/// [`UdpTransport`](super::UdpTransport) override uses `recvmmsg` into a caller-owned, reused
/// buffer ring — no per-packet allocation or syscall at line rate. The default does a single
/// scalar [`recv`](Self::recv) into `out[0]` (correct for the loopback transport + non-Linux).
fn recv_batch(&self, out: &mut [Vec<u8>], lens: &mut [usize]) -> std::io::Result<usize> {
if out.is_empty() {
return Ok(0);
}
match self.recv()? {
Some(pkt) => {
let n = pkt.len().min(out[0].len());
out[0][..n].copy_from_slice(&pkt[..n]);
lens[0] = n;
Ok(1)
}
None => Ok(0),
}
}
}