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>
This commit is contained in:
2026-07-16 20:08:29 +02:00
parent 07e2836601
commit ecfa71212d
67 changed files with 9456 additions and 8062 deletions
@@ -61,7 +61,7 @@ pub struct GamepadFrame {
// These are `pub const` aliases rather than a `pub use` re-export on purpose: on Windows the sole
// consumer (the Linux uinput map) is cfg'd out, and an unused re-export lints as an error there,
// whereas an unused `pub const` does not. The values still come only from core, so they can't drift;
// the exact wire values are pinned by `punktfunk1.rs::gamepad_wire_bits_are_pinned`.
// the exact wire values are pinned by `native.rs::gamepad_wire_bits_are_pinned`.
use punktfunk_core::input::gamepad as wire;
pub const BTN_DPAD_UP: u32 = wire::BTN_DPAD_UP;
pub const BTN_DPAD_DOWN: u32 = wire::BTN_DPAD_DOWN;
+20 -10
View File
@@ -194,13 +194,13 @@ impl AppState {
/// #5/#9) — so it is **opt-in** (`serve --gamestream`) and gated on a trusted LAN.
pub fn serve(
mgmt: crate::mgmt::Options,
native: crate::punktfunk1::NativeServe,
native: crate::native::NativeServe,
gamestream: bool,
) -> Result<()> {
let host = Host::detect()?;
let identity = cert::ServerIdentity::load_or_create().context("host certificate")?;
// The shared streaming-stats recorder: one handle for the mgmt API, the GameStream encode loop
// (via `AppState`), and the native punktfunk/1 loops (passed to `punktfunk1::serve`).
// (via `AppState`), and the native punktfunk/1 loops (passed to `native::serve`).
let stats = crate::stats_recorder::StatsRecorder::new(crate::stats_recorder::default_dir());
let state = Arc::new(AppState::new(host, identity, stats.clone()));
// The native plane always runs, so the shared native-pairing handle (linking the QUIC ceremony
@@ -241,8 +241,15 @@ pub fn serve(
rt.block_on(async move {
// rustls needs a process-wide crypto provider before any TLS config is built.
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let native_opts = crate::punktfunk1::native_serve_opts(&native);
if gamestream {
let native_opts = crate::native::native_serve_opts(&native);
// Lifecycle events (RFC §4): `host.started` as the serve planes come up; `host.stopping`
// when they wind down (clean end OR error exit) — the ring holds it for a consumer that
// reconnects, and a graceful-signal path can move the emit earlier when one exists.
crate::events::emit(crate::events::EventKind::HostStarted {
version: env!("CARGO_PKG_VERSION").to_string(),
gamestream,
});
let served: anyhow::Result<()> = if gamestream {
// Unified host: GameStream compat planes + native + mgmt. The `_nvstream` advert is
// fatal on failure when enabled (Moonlight clients can't find the host without it) —
// `--no-mdns` / PUNKTFUNK_MDNS=0 skips it for multicast-dead environments (stock
@@ -270,8 +277,9 @@ pub fn serve(
stats.clone(),
gamestream
),
crate::punktfunk1::serve(native_opts, native.mgmt_port, np, stats.clone()),
)?;
crate::native::serve(native_opts, native.mgmt_port, np, stats.clone()),
)
.map(|_| ())
} else {
// Secure default: native punktfunk/1 + management API only (no GameStream surface).
tracing::info!(
@@ -287,10 +295,12 @@ pub fn serve(
stats.clone(),
gamestream
),
crate::punktfunk1::serve(native_opts, native.mgmt_port, np, stats.clone()),
)?;
}
Ok(())
crate::native::serve(native_opts, native.mgmt_port, np, stats.clone()),
)
.map(|_| ())
};
crate::events::emit(crate::events::EventKind::HostStopping);
served
})
}
@@ -266,6 +266,15 @@ impl Pairing {
super::save_paired(&store);
}
tracing::info!(uniqueid, "pairing phase 4 complete — client cert pinned");
// Lifecycle event, plane parity with `NativePairing::add` (RFC §4). GameStream
// pairing has no device name — the client's uniqueid is the identity it presents.
crate::events::emit(crate::events::EventKind::PairingCompleted {
device: crate::events::DeviceRef {
name: uniqueid.to_string(),
fingerprint: hex::encode(crypto::sha256(&[s.client_cert_der.as_slice()])),
plane: crate::events::Plane::Gamestream,
},
});
Ok(paired_xml("", true))
} else {
tracing::warn!(
+44 -7
View File
@@ -58,9 +58,31 @@ pub fn start(
.spawn(move || {
// Same scheduling posture as the native path's capture/encode thread (Linux nice -10 /
// Windows HIGHEST + session tuning) — GameStream previously ran unboosted on Linux.
crate::punktfunk1::boost_thread_priority(true);
crate::native::boost_thread_priority(true);
tracing::info!(?cfg, "video stream starting");
if let Err(e) = run(
// Lifecycle events, plane parity with the native loop (RFC §4): the RTSP layer
// carries no client device name, so `client` is empty here — the `plane` field is
// what hooks key on. `client.connected` fires alongside `stream.started` because a
// Moonlight client has no persistent connection to anchor it to.
let event_stream = crate::events::StreamRef {
mode: crate::events::mode_str(cfg.width, cfg.height, cfg.fps),
hdr: cfg.hdr,
client: String::new(),
app: app.as_ref().map(|a| a.title.clone()),
plane: crate::events::Plane::Gamestream,
};
let event_client = crate::events::ClientRef {
name: String::new(),
fingerprint: None,
plane: crate::events::Plane::Gamestream,
};
crate::events::emit(crate::events::EventKind::StreamStarted {
stream: event_stream.clone(),
});
crate::events::emit(crate::events::EventKind::ClientConnected {
client: event_client.clone(),
});
let result = run(
cfg,
app.as_ref(),
&running,
@@ -68,10 +90,25 @@ pub fn start(
&rfi_range,
&video_cap,
&stats,
) {
);
// A clean return is a stop (RTSP teardown / cancel / client unreachable) → `quit`;
// an error return is `error`. The compat plane can't tell a user stop from an idle
// vanish the way the native plane's typed close code can.
let reason = match &result {
Ok(()) => crate::events::DisconnectReason::Quit,
Err(_) => crate::events::DisconnectReason::Error,
};
if let Err(e) = result {
tracing::error!(error = %format!("{e:#}"), "video stream failed");
}
running.store(false, Ordering::SeqCst);
crate::events::emit(crate::events::EventKind::StreamStopped {
stream: event_stream,
});
crate::events::emit(crate::events::EventKind::ClientDisconnected {
client: event_client,
reason,
});
tracing::info!("video stream stopped");
});
}
@@ -227,7 +264,7 @@ fn run(
/// Open the virtual-display video source for a GameStream session: pick the LIVE compositor + normalize
/// the session env (apply_session_env/apply_input_env — gamescope ATTACH/resize, KWin/Mutter
/// retargeting) exactly like the native plane (punktfunk1.rs resolve_compositor), create a virtual
/// retargeting) exactly like the native plane (native.rs resolve_compositor), create a virtual
/// output at the client's mode, and capture it. Returns the capturer (it owns the output's keepalive;
/// the stateless VirtualDisplay factory is dropped here) plus the resolved compositor. An apps.json
/// entry can PIN a compositor (skips the live detect/retarget). Re-run on a mid-stream capture loss to
@@ -242,7 +279,7 @@ fn open_gs_virtual_source(
} else {
// Windows has a single virtual-display backend (pf-vdisplay); `vdisplay::open` ignores the
// compositor arg there, so short-circuit the Linux session-detection state machine with a
// placeholder — mirrors `punktfunk1::resolve_compositor`. Without this, the Linux `detect()`
// placeholder — mirrors `native::resolve_compositor`. Without this, the Linux `detect()`
// below bails on Windows ("could not detect compositor … XDG_CURRENT_DESKTOP=''"), which
// killed the GameStream video thread → black screen (the native plane was already guarded).
#[cfg(target_os = "windows")]
@@ -460,7 +497,7 @@ fn spawn_packetizer(
.name("punktfunk-pkt".into())
.spawn(move || {
// Above-normal, like the send thread — this stage is on the per-frame critical path.
crate::punktfunk1::boost_thread_priority(false);
crate::native::boost_thread_priority(false);
while let Ok(frame) = rx.recv() {
let mut batch: PacketBatch = Vec::new();
for (au, ft, idx) in frame.aus {
@@ -498,7 +535,7 @@ fn spawn_sender(
.spawn(move || {
// Transmit thread: above-normal, matching the native path's send thread (includes the
// Windows session tuning/MMCSS this used to call directly; adds the Linux nice -5).
crate::punktfunk1::boost_thread_priority(false);
crate::native::boost_thread_priority(false);
let budget = frame_interval.mul_f32(0.75);
let cfg = crate::send_pacing::PaceCfg {
burst_bytes: None, // no microburst stage — the whole frame spreads