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>
This commit is contained in:
@@ -113,6 +113,10 @@ pub struct AppState {
|
||||
/// Set by the control stream when the client requests an IDR / invalidates reference
|
||||
/// frames (recovery after loss); the video thread forces a keyframe and clears it.
|
||||
pub force_idr: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
/// A client reference-frame-invalidation request carrying the lost frame range (0x0301). The
|
||||
/// video thread drains it and calls `Encoder::invalidate_ref_frames`, falling back to a full
|
||||
/// IDR when the encoder can't invalidate (range too old / no NVENC RFI). `None` = nothing pending.
|
||||
pub rfi_range: std::sync::Arc<std::sync::Mutex<Option<(i64, i64)>>>,
|
||||
/// Persistent screen capturer, reused across streams so reconnects don't spawn a second
|
||||
/// (conflicting) screencast session. The video thread borrows it for the stream's duration
|
||||
/// and returns it; `set_active` gates its cost while idle.
|
||||
@@ -138,6 +142,7 @@ impl AppState {
|
||||
streaming: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
audio_streaming: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
force_idr: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
rfi_range: std::sync::Arc::new(std::sync::Mutex::new(None)),
|
||||
video_cap: std::sync::Arc::new(std::sync::Mutex::new(None)),
|
||||
audio_cap: std::sync::Arc::new(std::sync::Mutex::new(None)),
|
||||
}
|
||||
@@ -293,18 +298,30 @@ fn load_paired() -> Vec<Vec<u8>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist the paired-client allow-list (called after each successful pairing).
|
||||
/// Persist the paired-client allow-list (called after each successful pairing). Written
|
||||
/// atomically (temp file + rename) so a crash mid-write can't truncate `paired.json` — a partial
|
||||
/// write would otherwise lock out every paired client until they re-pair.
|
||||
pub(crate) fn save_paired(paired: &[Vec<u8>]) {
|
||||
let Some(path) = paired_path() else { return };
|
||||
if let Some(dir) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(dir);
|
||||
}
|
||||
match serde_json::to_vec(paired) {
|
||||
Ok(bytes) => {
|
||||
if let Err(e) = std::fs::write(&path, bytes) {
|
||||
tracing::warn!(error = %e, "persisting pairings failed");
|
||||
}
|
||||
let bytes = match serde_json::to_vec(paired) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "serializing pairings failed");
|
||||
return;
|
||||
}
|
||||
Err(e) => tracing::warn!(error = %e, "serializing pairings failed"),
|
||||
};
|
||||
// Write to a sibling temp file, then rename over the target (atomic replace on Unix and
|
||||
// Windows). Never write `path` in place.
|
||||
let tmp = path.with_extension("json.tmp");
|
||||
if let Err(e) = std::fs::write(&tmp, &bytes) {
|
||||
tracing::warn!(error = %e, "persisting pairings failed (temp write)");
|
||||
return;
|
||||
}
|
||||
if let Err(e) = std::fs::rename(&tmp, &path) {
|
||||
tracing::warn!(error = %e, "persisting pairings failed (rename)");
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user