refactor: drop milestone names + consolidate clients; loss-recovery & rumble fixes
apple / swift (push) Failing after 40s
audit / cargo-audit (push) Failing after 1m12s
windows-msix / package (push) Successful in 1m37s
windows / build (push) Successful in 1m14s
android / android (push) Successful in 4m48s
ci / web (push) Successful in 27s
ci / rust (push) Successful in 4m21s
ci / docs-site (push) Successful in 31s
ci / bench (push) Successful in 4m39s
decky / build-publish (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
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 4s
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 19s
deb / build-publish (push) Successful in 6m3s
flatpak / build-publish (push) Successful in 4m13s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m15s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m16s
docker / deploy-docs (push) Successful in 18s

Two bodies of work in one commit (the rename moved files the fixes also touched).

Naming/structure cleanup (pre-launch):
- Host modules m3.rs->punktfunk1.rs, m0.rs->spike.rs; CLI m3-host->punktfunk1-host,
  m0->spike; bare `punktfunk-host` now prints help. Types M3Options/M3Source->
  Punktfunk1Options/Punktfunk1Source.
- Clients consolidated out of crates/ into clients/: punktfunk-client-rs->
  clients/probe (crate punktfunk-probe), client-linux->clients/linux,
  client-windows->clients/windows, punktfunk-android->clients/android/native
  (crate punktfunk-client-android; kept [lib] name=punktfunk_android so the JNI
  contract is unchanged). crates/ now holds only core + host.
- Milestone codes M0-M4 purged from code/CLI/CLAUDE.md/README/docs/docs-site,
  kept only in docs/implementation-plan.md. docs/m2-plan.md->
  docs/gamestream-host-plan.md. CI/gradle/flatpak paths updated.

Client loss-recovery (video froze and never recovered after a brief drop):
- Export punktfunk_connection_frames_dropped through the C ABI (the core already
  tracked it for the client keyframe-recovery loop; it was never reachable from
  the ABI clients). Regenerated punktfunk_core.h.
- Apple (StreamPump + Stage2Pipeline) and Android (decode.rs) now poll
  frames_dropped and request a keyframe when it climbs -- the same loss-driven
  recovery Linux/Windows already had. Under infinite GOP the decoder silently
  conceals reference-missing frames, so the decode-error trigger rarely fires.

Apple rumble robustness (worked then went spotty -- DualSense + Xbox):
- Add CHHapticEngine stopped/reset handlers (rebuild on app background / audio
  interruption / server reset) and drop the permanent `broken` latch on a
  transient drive failure; latch only when the controller truly has no haptics.
- Surface swallowed SDL set_rumble errors on Linux/Windows + diagnostic logging.

Verified: cargo build/clippy/fmt --workspace, C-ABI harness, header drift.
Not runnable on this box (verify in CI): Gitea workflows, gradle/Android,
flatpak, Swift/decky.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 21:03:55 +00:00
parent 1faa6c6ad4
commit 9c8fa9340c
110 changed files with 534 additions and 341 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ use anyhow::Result;
/// Opus/GameStream audio is 48 kHz.
pub const SAMPLE_RATE: u32 = 48_000;
/// Stereo channel count — the default and the punktfunk/1 (M3) audio plane's fixed layout.
/// Stereo channel count — the default and the punktfunk/1 audio plane's fixed layout.
pub const CHANNELS: usize = 2;
/// Produces interleaved `f32` PCM at [`SAMPLE_RATE`] in the channel count it was opened
+3 -3
View File
@@ -1,4 +1,4 @@
//! Frame capture (plan §7). On Linux: a PipeWire ScreenCast portal stream. M0 uses the
//! Frame capture (plan §7). On Linux: a PipeWire ScreenCast portal stream. The spike uses the
//! CPU-copy fallback (the portal delivers a CPU buffer; the encoder uploads it to the GPU
//! internally). Zero-copy dmabuf→NVENC import is deferred (plan §9 risk).
@@ -45,7 +45,7 @@ impl PixelFormat {
}
/// A captured frame. [`format`](Self::format)/dimensions describe the pixels regardless of
/// where they live — [`payload`](Self::payload) is either a CPU buffer (the M0/fallback path)
/// where they live — [`payload`](Self::payload) is either a CPU buffer (the spike/fallback path)
/// or a GPU buffer already on the device (the zero-copy path, plan §9).
pub struct CapturedFrame {
pub width: u32,
@@ -103,7 +103,7 @@ pub trait Capturer: Send {
fn set_active(&self, _active: bool) {}
}
/// A deterministic moving test pattern (BGRx). Lets M0 exercise the encode → file →
/// A deterministic moving test pattern (BGRx). Lets the spike exercise the encode → file →
/// `punktfunk_core` path with no live capture session, and produces obviously non-static
/// content (a sweeping bar + animated gradient) so the encoded output is verifiable.
pub struct SyntheticCapturer {
+1 -1
View File
@@ -1319,7 +1319,7 @@ pub struct DuplCapturer {
ever_got_frame: bool,
/// Consecutive rebuilds that produced a BORN-LOST duplication (created OK, but its first
/// AcquireNextFrame instantly returned ACCESS_LOST). On the NORMAL desktop this is the hybrid
/// reparent/flip storm — once it persists, `acquire` returns Err so the m3 loop cold-rebuilds the
/// reparent/flip storm — once it persists, `acquire` returns Err so the punktfunk1 loop cold-rebuilds the
/// whole pipeline (new device/output) instead of spinning on a dead dup forever (the bug where the
/// stream froze on the last frame). Reset to 0 by any real frame. NOT armed on the secure
/// (Winlogon) desktop, where a long static dwell is legitimate and must never end the session.
@@ -2,7 +2,7 @@
//! docs/windows-secure-desktop.md — step 4).
//!
//! WGC won't activate under the SYSTEM account, so the SYSTEM host can't capture the normal desktop
//! itself. Instead it spawns `m3-host wgc-helper` in the **interactive user session** (so WGC works)
//! itself. Instead it spawns `punktfunk-host wgc-helper` in the **interactive user session** (so WGC works)
//! via `CreateProcessAsUserW`, with the helper's **stdout** redirected to an anonymous pipe the host
//! reads. The helper ships framed Annex-B access units; this module parses them back into AUs the
//! host relays onto the live QUIC session (same `EncodedFrame` flow, just sourced over a pipe instead
+1 -1
View File
@@ -1,5 +1,5 @@
//! Hardware video encode (plan §7). Binds FFmpeg (NVENC); never rewrites codecs.
//! Low-latency preset, B-frames off. M0 feeds BGRx CPU frames directly — `*_nvenc`
//! Low-latency preset, B-frames off. The spike feeds BGRx CPU frames directly — `*_nvenc`
//! accepts `bgr0` input and converts to YUV on the GPU, so no host-side swscale is
//! needed (dmabuf zero-copy import is deferred; plan §9).
+7 -4
View File
@@ -1,10 +1,10 @@
//! GameStream (P1) control plane — what a stock Moonlight/Artemis client talks to around
//! the media streams: mDNS discovery, the nvhttp serverinfo + pairing HTTP(S) API, RTSP,
//! and the ENet control stream. `tokio`/`axum` live here (control plane, I/O-bound — never
//! the per-frame hot path; that is `punktfunk_core`'s P1 wire codec). See `docs/m2-plan.md`.
//! the per-frame hot path; that is `punktfunk_core`'s P1 wire codec). See `docs/gamestream-host-plan.md`.
//!
//! Status: P1.1 — mDNS `_nvstream._tcp` advertisement + `/serverinfo`. Pairing, RTSP, and
//! the media streams follow (see the M2 task list / plan).
//! the media streams follow (see the GameStream host task list / plan).
pub mod apps;
// Platform-neutral wire/negotiation logic + the Linux capture/encode pipeline (non-Linux
@@ -149,7 +149,10 @@ impl AppState {
/// QUIC server on `cfg.port` in the same process, sharing one [`crate::native_pairing`] handle with
/// the management API so the web console can arm pairing and show the PIN. `None` = GameStream only
/// (the mgmt API's native endpoints report `enabled: false`).
pub fn serve(mgmt: crate::mgmt::Options, native: Option<crate::m3::NativeServe>) -> Result<()> {
pub fn serve(
mgmt: crate::mgmt::Options,
native: Option<crate::punktfunk1::NativeServe>,
) -> Result<()> {
let host = Host::detect()?;
let identity = cert::ServerIdentity::load_or_create().context("host certificate")?;
let state = Arc::new(AppState::new(host, identity));
@@ -187,7 +190,7 @@ pub fn serve(mgmt: crate::mgmt::Options, native: Option<crate::m3::NativeServe>)
tokio::try_join!(
nvhttp::run(state.clone()),
crate::mgmt::run(state.clone(), mgmt, Some(np.clone())),
crate::m3::serve(crate::m3::native_serve_opts(&cfg), np),
crate::punktfunk1::serve(crate::punktfunk1::native_serve_opts(&cfg), np),
)?;
}
_ => {
@@ -1,6 +1,6 @@
//! The video data plane: on RTSP PLAY, learn the client's UDP endpoint (it pings the video
//! port), then run capture → NVENC encode → [`VideoPacketizer`] → UDP send. The source is
//! either real portal desktop capture (`PUNKTFUNK_VIDEO_SOURCE=portal`, the M0 PipeWire path) or
//! either real portal desktop capture (`PUNKTFUNK_VIDEO_SOURCE=portal`, the portal PipeWire path) or
//! a synthetic test pattern (default). Runs on its own native thread.
use super::video::{FrameType, VideoPacketizer};
+33 -31
View File
@@ -6,9 +6,10 @@
//! `#[cfg(target_os = "linux")]`; the crate compiles everywhere so the workspace builds
//! on non-Linux dev machines — it just can't run the pipeline there.
//!
//! Status: M0. The `m0` subcommand runs the capture→encode→file pipeline spike and feeds
//! the encoded AUs through a `punktfunk_core` loopback. M2 wires the full P1 host that a stock
//! Moonlight client connects to.
//! Subcommands: `serve` runs the GameStream-compatible host + management REST API (and, with
//! `--native`, the native punktfunk/1 host in-process); `punktfunk1-host` runs the native
//! punktfunk/1 host standalone; `spike` is a capture→encode→file pipeline dev tool that also
//! round-trips the encoded AUs through a `punktfunk_core` loopback.
// Scaffold: trait methods and config paths are defined ahead of their backends.
#![allow(dead_code)]
@@ -24,15 +25,15 @@ mod encode;
mod gamestream;
mod inject;
mod library;
mod m0;
mod m3;
mod mgmt;
mod mgmt_token;
mod native_pairing;
mod pipeline;
mod punktfunk1;
mod pwinit;
#[cfg(target_os = "windows")]
mod service;
mod spike;
mod vdisplay;
#[cfg(target_os = "windows")]
mod wgc_helper;
@@ -41,7 +42,7 @@ mod zerocopy;
use anyhow::{bail, Context, Result};
use encode::Codec;
use m0::{Options, Source};
use spike::{Options, Source};
use std::path::PathBuf;
fn main() {
@@ -185,10 +186,10 @@ fn real_main() -> Result<()> {
println!("dualsense-test: done");
Ok(())
}
// M0 pipeline spike.
Some("m0") => m0::run(parse_m0(&args[1..])?),
// M3: native punktfunk/1 host (QUIC control plane + UDP data plane).
Some("m3-host") => {
// Capture→encode→file pipeline spike (dev tool).
Some("spike") => spike::run(parse_spike(&args[1..])?),
// Native punktfunk/1 host (QUIC control plane + UDP data plane).
Some("punktfunk1-host") => {
let get = |flag: &str| {
args.iter()
.skip_while(|a| *a != flag)
@@ -196,10 +197,10 @@ fn real_main() -> Result<()> {
.map(String::as_str)
};
let source = match get("--source") {
Some("virtual") => m3::M3Source::Virtual,
_ => m3::M3Source::Synthetic,
Some("virtual") => punktfunk1::Punktfunk1Source::Virtual,
_ => punktfunk1::Punktfunk1Source::Synthetic,
};
m3::run(m3::M3Options {
punktfunk1::run(punktfunk1::Punktfunk1Options {
port: get("--port").and_then(|s| s.parse().ok()).unwrap_or(9777),
source,
seconds: get("--seconds").and_then(|s| s.parse().ok()).unwrap_or(30),
@@ -209,7 +210,7 @@ fn real_main() -> Result<()> {
.unwrap_or(0),
max_concurrent: get("--max-concurrent")
.and_then(|s| s.parse().ok())
.unwrap_or(m3::DEFAULT_MAX_CONCURRENT),
.unwrap_or(punktfunk1::DEFAULT_MAX_CONCURRENT),
// Secure by default: REQUIRE PIN pairing (reject unpaired clients) unless
// --allow-tofu opts into trust-on-first-use — the host then accepts unpaired
// clients and advertises pair=optional. Pairing is always armed so a PIN is
@@ -259,8 +260,9 @@ fn real_main() -> Result<()> {
print_usage();
Ok(())
}
// Bare flags (no subcommand) default to the m0 spike for back-compat.
Some(_) => m0::run(parse_m0(&args)?),
// Unknown subcommand → usage. (No implicit default; a bare `punktfunk-host` with no
// args hits the None arm above and prints help.)
Some(other) => bail!("unknown command '{other}' (try --help)"),
}
}
@@ -320,7 +322,7 @@ fn input_test() -> Result<()> {
/// the native punktfunk/1 host in-process (`--native`, the unified host). Returns the mgmt options
/// and the native host config (`None` = GameStream only). Native pairing is **required by default**
/// (an open host any LAN device can stream from is insecure); `--open` turns it off.
fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option<m3::NativeServe>)> {
fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option<punktfunk1::NativeServe>)> {
let mut opts = mgmt::Options::default();
let mut native_port: Option<u16> = None;
let mut open = false;
@@ -377,14 +379,14 @@ fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option<m3::NativeServe
if opts.token.is_none() {
opts.token = Some(crate::mgmt_token::load_or_generate()?);
}
let native = native_port.map(|port| m3::NativeServe {
let native = native_port.map(|port| punktfunk1::NativeServe {
port,
require_pairing: !open,
});
Ok((opts, native))
}
fn parse_m0(args: &[String]) -> Result<Options> {
fn parse_spike(args: &[String]) -> Result<Options> {
let mut source = Source::Portal;
let mut width = 1920u32;
let mut height = 1080u32;
@@ -465,7 +467,7 @@ fn parse_m0(args: &[String]) -> Result<Options> {
Codec::H265 => "h265",
Codec::Av1 => "obu",
};
PathBuf::from(format!("/tmp/punktfunk-m0.{ext}"))
PathBuf::from(format!("/tmp/punktfunk-spike.{ext}"))
});
Ok(Options {
@@ -486,12 +488,12 @@ fn print_usage() {
"punktfunk-host — Linux streaming host
USAGE:
punktfunk-host serve [OPTIONS] GameStream host control plane (M2: mDNS + serverinfo …)
+ the management REST API
punktfunk-host openapi print the management API's OpenAPI document (codegen)
punktfunk-host m3-host [OPTIONS] native punktfunk/1 host (QUIC control plane + UDP data plane)
punktfunk-host probe-compositor exit 0 iff the compositor is up + ready (session-bringup gate)
punktfunk-host m0 [OPTIONS] M0 capture→encode→file pipeline spike
punktfunk-host serve [OPTIONS] GameStream host control plane (mDNS + serverinfo …)
+ the management REST API
punktfunk-host openapi print the management API's OpenAPI document (codegen)
punktfunk-host punktfunk1-host [OPTIONS] native punktfunk/1 host (QUIC control + UDP data plane)
punktfunk-host probe-compositor exit 0 iff the compositor is up + ready (bringup gate)
punktfunk-host spike [OPTIONS] capture→encode→file pipeline spike (dev tool)
SERVE OPTIONS:
--mgmt-bind <IP:PORT> management API address (default: 127.0.0.1:47990)
@@ -503,7 +505,7 @@ SERVE OPTIONS:
--open disable mandatory native pairing (default: pairing REQUIRED —
an open host any LAN device can stream from is insecure)
M3-HOST OPTIONS:
PUNKTFUNK1-HOST OPTIONS:
--port <N> QUIC listen port (default: 9777)
--source <synthetic|virtual> test frames, or virtual display + NVENC (default: synthetic)
--seconds <N> per-session stream duration, virtual source (default: 30)
@@ -516,7 +518,7 @@ M3-HOST OPTIONS:
unpaired clients and logs a 4-digit pairing PIN at startup;
TOFU without pairing is insecure on a LAN
M0 OPTIONS:
SPIKE OPTIONS:
--source <synthetic|portal|kwin-virtual>
frame source (default: portal). 'kwin-virtual' creates a
KWin virtual output at --width x --height and captures it
@@ -525,7 +527,7 @@ M0 OPTIONS:
--codec <h264|h265|av1> NVENC codec (default: h265)
--bitrate <MBPS> target bitrate in Mbps (default: 20)
--width <W> --height <H> synthetic source size (default: 1920x1080)
--out <PATH> raw Annex-B output (default: /tmp/punktfunk-m0.<ext>)
--out <PATH> raw Annex-B output (default: /tmp/punktfunk-spike.<ext>)
--no-loopback skip the punktfunk_core round-trip verification
-h, --help this help
@@ -534,8 +536,8 @@ NOTES:
(see docs/linux-setup.md). 'synthetic' needs no capture session and always runs.
Encoded AUs are written to a playable file AND (unless --no-loopback) fed through a
punktfunk_core host→client loopback that reassembles and byte-verifies each one.
Both 'serve --native' and 'm3-host' advertise the native service over mDNS
(_punktfunk._udp) for client auto-discovery — 'punktfunk-client-rs --discover' lists them."
Both 'serve --native' and 'punktfunk1-host' advertise the native service over mDNS
(_punktfunk._udp) for client auto-discovery — 'punktfunk-probe --discover' lists them."
);
#[cfg(target_os = "windows")]
eprintln!(
+1 -1
View File
@@ -1,6 +1,6 @@
//! Shared native (`punktfunk/1`) pairing state — the on-demand arming PIN (with expiry) plus the
//! persistent paired-clients store. One [`NativePairing`] handle is shared by the punktfunk/1 QUIC
//! accept loop ([`crate::m3`]) and the management API ([`crate::mgmt`]), so an operator can **arm
//! accept loop ([`crate::punktfunk1`]) and the management API ([`crate::mgmt`]), so an operator can **arm
//! pairing and read the PIN from the web console** instead of the service log.
//!
//! The PIN direction is inherent to the SPAKE2 ceremony: the *host* mints the PIN and the *client*
@@ -1,4 +1,4 @@
//! M3 — the `punktfunk/1` native host: QUIC control plane + the hardened M1 data plane over UDP.
//! The `punktfunk/1` native host: QUIC control plane + the hardened core data plane over UDP.
//! This is punktfunk's own protocol, past the GameStream compatibility layer:
//!
//! * the Welcome negotiates **GF(2¹⁶) Leopard FEC** (inexpressible in GameStream) + AES-GCM;
@@ -9,9 +9,9 @@
//! * video frames carry a wall-clock `pts_ns`, so a same-host client measures the full
//! capture→encode→FEC→UDP→reassemble latency per frame.
//!
//! `punktfunk-host m3-host [--port 9777] [--source synthetic|virtual] [--seconds 30]
//! `punktfunk-host punktfunk1-host [--port 9777] [--source synthetic|virtual] [--seconds 30]
//! [--frames 300]` serves sessions back to back (one at a time — the virtual output and
//! encoder are single-tenant); `punktfunk-client-rs --connect host:9777` is the counterpart.
//! encoder are single-tenant); `punktfunk-probe --connect host:9777` is the counterpart.
//! The data plane runs on native threads (no async on the frame path).
//!
//! Alongside video + input, a session carries **audio** (desktop Opus, 5 ms frames, host →
@@ -37,16 +37,16 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum M3Source {
pub enum Punktfunk1Source {
/// Deterministic test frames (protocol verification; the client byte-checks them).
Synthetic,
/// Real capture: virtual display at the client's requested mode → NVENC.
Virtual,
}
pub struct M3Options {
pub struct Punktfunk1Options {
pub port: u16,
pub source: M3Source,
pub source: Punktfunk1Source,
/// Virtual-source stream duration.
pub seconds: u32,
/// Synthetic-source frame count.
@@ -97,7 +97,7 @@ fn now_ns() -> u64 {
.unwrap_or(0)
}
pub fn run(opts: M3Options) -> Result<()> {
pub fn run(opts: Punktfunk1Options) -> Result<()> {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
@@ -138,10 +138,10 @@ pub(crate) struct NativeServe {
/// overflow clients wait in the accept queue. Override with `--max-concurrent`.
pub(crate) const DEFAULT_MAX_CONCURRENT: usize = 4;
pub(crate) fn native_serve_opts(cfg: &NativeServe) -> M3Options {
M3Options {
pub(crate) fn native_serve_opts(cfg: &NativeServe) -> Punktfunk1Options {
Punktfunk1Options {
port: cfg.port,
source: M3Source::Virtual,
source: Punktfunk1Source::Virtual,
seconds: 7 * 24 * 3600, // per-session cap; large enough not to cut a live stream
frames: 0,
max_sessions: 0,
@@ -153,7 +153,7 @@ pub(crate) fn native_serve_opts(cfg: &NativeServe) -> M3Options {
}
}
pub(crate) async fn serve(opts: M3Options, np: Arc<NativePairing>) -> Result<()> {
pub(crate) async fn serve(opts: Punktfunk1Options, np: Arc<NativePairing>) -> Result<()> {
let identity = crate::gamestream::cert::ServerIdentity::load_or_create()
.context("load host identity (~/.config/punktfunk)")?;
let fingerprint = endpoint::fingerprint_of_pem(&identity.cert_pem)
@@ -427,7 +427,7 @@ async fn pair_ceremony(
#[allow(clippy::too_many_arguments)]
async fn serve_session(
conn: quinn::Connection,
opts: &M3Options,
opts: &Punktfunk1Options,
audio_cap: &AudioCapSlot,
inj_tx: std::sync::mpsc::Sender<InputEvent>,
mic_tx: std::sync::mpsc::Sender<Vec<u8>>,
@@ -514,7 +514,7 @@ async fn serve_session(
// can report what we'll actually drive. Only the Virtual source has a compositor; the
// synthetic source has no virtual output. Blocking probes → spawn_blocking.
let compositor = match source {
M3Source::Virtual => {
Punktfunk1Source::Virtual => {
let pref = hello.compositor;
Some(
tokio::task::spawn_blocking(move || resolve_compositor(pref))
@@ -522,7 +522,7 @@ async fn serve_session(
.context("resolve compositor task")??,
)
}
M3Source::Synthetic => None,
Punktfunk1Source::Synthetic => None,
};
// Resolve a requested library launch (the client sends only the store-qualified id;
@@ -600,8 +600,8 @@ async fn serve_session(
key,
salt: *b"pkf1",
frames: match source {
M3Source::Synthetic => frames,
M3Source::Virtual => 0, // unbounded — client streams until we close
Punktfunk1Source::Synthetic => frames,
Punktfunk1Source::Virtual => 0, // unbounded — client streams until we close
},
// Report the resolved backends back to the client (compositor: Auto for the
// synthetic source).
@@ -726,7 +726,7 @@ async fn serve_session(
let conn = conn.clone();
let gamepad = welcome.gamepad;
std::thread::Builder::new()
.name("punktfunk-m3-input".into())
.name("punktfunk1-input".into())
.spawn(move || input_thread(input_rx, rich_rx, conn, inj_tx, gamepad))
.context("spawn input thread")?
};
@@ -778,12 +778,12 @@ async fn serve_session(
// → host→client QUIC datagrams, on its own native thread. Best-effort on every failure
// (no PipeWire audio, spawn error): the session continues without audio — and a spawn
// error must NOT early-return here, the threads above are already running.
let audio_handle = if opts.source == M3Source::Virtual {
let audio_handle = if opts.source == Punktfunk1Source::Virtual {
let conn = conn.clone();
let stop = stop.clone();
let cap = audio_cap.clone();
std::thread::Builder::new()
.name("punktfunk-m3-audio".into())
.name("punktfunk1-audio".into())
.spawn(move || audio_thread(conn, stop, cap))
.map_err(|e| tracing::error!(error = %e, "audio thread spawn failed — session continues without audio"))
.ok()
@@ -794,7 +794,7 @@ async fn serve_session(
// Test hook (synthetic source only): a scripted feedback burst on the host→client
// planes — rumble (0xCA) + DualSense HID-output (0xCD) — so loopback tests can assert
// the client's feedback path without a real game writing output reports to a real pad.
if opts.source == M3Source::Synthetic
if opts.source == Punktfunk1Source::Synthetic
&& std::env::var("PUNKTFUNK_TEST_FEEDBACK").as_deref() == Ok("1")
{
use punktfunk_core::quic::HidOutput;
@@ -852,14 +852,14 @@ async fn serve_session(
let mut session = Session::new(cfg, Box::new(transport))
.map_err(|e| anyhow!("host session: {e:?}"))?;
match source {
M3Source::Synthetic => synthetic_stream(
Punktfunk1Source::Synthetic => synthetic_stream(
&mut session,
frames,
&stop_stream,
&probe_rx,
&probe_result_tx,
),
M3Source::Virtual => {
Punktfunk1Source::Virtual => {
let compositor = compositor
.expect("the Virtual source resolves a compositor during the handshake");
virtual_stream(
@@ -986,7 +986,7 @@ impl InjectorService {
fn start() -> InjectorService {
let (tx, rx) = std::sync::mpsc::channel::<InputEvent>();
if let Err(e) = std::thread::Builder::new()
.name("punktfunk-m3-injector".into())
.name("punktfunk1-injector".into())
.spawn(move || injector_service_thread(rx))
{
tracing::error!(error = %e, "injector service thread spawn failed — pointer/keyboard input disabled");
@@ -1080,7 +1080,7 @@ impl MicService {
fn start() -> MicService {
let (tx, rx) = std::sync::mpsc::channel::<Vec<u8>>();
if let Err(e) = std::thread::Builder::new()
.name("punktfunk-m3-mic".into())
.name("punktfunk1-mic".into())
.spawn(move || mic_service_thread(rx))
{
tracing::error!(error = %e, "mic service thread spawn failed — mic passthrough disabled");
@@ -2117,7 +2117,7 @@ fn virtual_stream(
let _watcher = if watch {
let stop = stop.clone();
std::thread::Builder::new()
.name("punktfunk-m3-watcher".into())
.name("punktfunk1-watcher".into())
.spawn(move || session_watcher_loop(session_tx, stop))
.ok()
} else {
@@ -3014,9 +3014,9 @@ mod tests {
use punktfunk_core::error::PunktfunkStatus;
let host = std::thread::spawn(|| {
run(M3Options {
run(Punktfunk1Options {
port: 19777,
source: M3Source::Synthetic,
source: Punktfunk1Source::Synthetic,
seconds: 0,
frames: 25,
max_sessions: 3,
@@ -3182,9 +3182,9 @@ mod tests {
.build()
.unwrap();
rt.block_on(serve(
M3Options {
Punktfunk1Options {
port: 19779,
source: M3Source::Synthetic,
source: Punktfunk1Source::Synthetic,
seconds: 0,
frames: 25,
max_sessions: 2, // the knock + the post-approval session
@@ -3268,9 +3268,9 @@ mod tests {
use punktfunk_core::quic::endpoint;
let host = std::thread::spawn(|| {
run(M3Options {
run(Punktfunk1Options {
port: 19778,
source: M3Source::Synthetic,
source: Punktfunk1Source::Synthetic,
seconds: 0,
frames: 25,
max_sessions: 4,
@@ -1,9 +1,9 @@
//! M0 — the pipeline spike (plan §8): capture → NVENC encode → playable file, with the
//! The pipeline spike (plan §8): capture → NVENC encode → playable file, with the
//! encoded access units also fed through a `punktfunk_core` host→client `Session` over an
//! in-process loopback to prove the core's FEC + packetize + reassemble path on real
//! encoder output.
//!
//! This is the spike runner, not the M2 hot path: it drives the stages on one thread (the
//! This is the spike runner, not the production host path: it drives the stages on one thread (the
//! per-stage-thread pipeline with bounded channels is [`crate::pipeline`]). Source is
//! either a synthetic BGRx test pattern (no capture session needed) or the live xdg
//! ScreenCast portal monitor.
@@ -52,12 +52,12 @@ pub fn run(opts: Options) -> Result<()> {
width = opts.width,
height = opts.height,
fps = opts.fps,
"M0 source: synthetic BGRx test pattern"
"spike source: synthetic BGRx test pattern"
);
Box::new(SyntheticCapturer::new(opts.width, opts.height, opts.fps))
}
Source::Portal => {
tracing::info!("M0 source: xdg ScreenCast portal (live monitor)");
tracing::info!("spike source: xdg ScreenCast portal (live monitor)");
capture::open_portal_monitor().context("open portal capturer")?
}
Source::KwinVirtual => {
@@ -66,7 +66,7 @@ pub fn run(opts: Options) -> Result<()> {
width = opts.width,
height = opts.height,
?compositor,
"M0 source: virtual output (PUNKTFUNK_COMPOSITOR)"
"spike source: virtual output (PUNKTFUNK_COMPOSITOR)"
);
let mut vd = crate::vdisplay::open(compositor).context("open virtual display")?;
let vout = vd
@@ -104,7 +104,7 @@ pub fn run(opts: Options) -> Result<()> {
opts.fps,
opts.bitrate_bps,
first.is_cuda(),
8, // m0 synthetic harness: 8-bit
8, // spike synthetic harness: 8-bit
)
.context("open encoder")?;
@@ -147,7 +147,7 @@ pub fn run(opts: Options) -> Result<()> {
out = %opts.out.display(),
elapsed_s = format!("{elapsed:.2}"),
encode_fps = format!("{:.1}", stats.encoded as f64 / elapsed.max(1e-9)),
"M0 capture→encode→file complete"
"spike capture→encode→file complete"
);
if let Some(lb) = lb {
@@ -194,7 +194,7 @@ fn drain_encoder(
/// A host↔client `punktfunk_core` pair over a lossless in-process loopback. Each encoded AU is
/// FEC-protected, packetized, sent, then reassembled on the client and byte-compared to the
/// original — exercising the core on real encoder output (the M0 "feed into a Session" goal).
/// original — exercising the core on real encoder output (the spike "feed into a Session" goal).
struct Loopback {
host: Session,
client: Session,
+1 -1
View File
@@ -125,7 +125,7 @@ impl Compositor {
/// installed (it spawns a nested session — independent of the running desktop), plus the live
/// session's own compositor (KWin / Mutter / wlroots) when the host runs inside it. Cheap,
/// side-effect-free probes — safe to call per management request. A concrete client preference
/// is validated against this set before it's honored (see the m3 handshake's resolution).
/// is validated against this set before it's honored (see the punktfunk/1 handshake's resolution).
pub fn available() -> Vec<Compositor> {
#[cfg(target_os = "linux")]
{
@@ -40,7 +40,7 @@ fn chooser_file() -> String {
}
/// The managed xdpw config: per-session output selection with no GUI. The `|| echo` fallback
/// keeps plain portal capture (`--source portal`, M0 flow) working when no session has written
/// keeps plain portal capture (`--source portal` flow) working when no session has written
/// the chooser file. xdpw runs `chooser_cmd` via `/bin/sh -c`, reads stdout.
fn xdpw_config() -> String {
format!(
+1 -1
View File
@@ -50,7 +50,7 @@ pub fn run(opts: HelperOptions) -> Result<()> {
// path. Elevate its OS priority so a CPU-heavy game can't deschedule it and delay submission (which
// would leave our HIGH GPU priority with nothing queued to prioritise). Apollo's capture thread is
// likewise CRITICAL.
crate::m3::boost_thread_priority(true);
crate::punktfunk1::boost_thread_priority(true);
// Capture the EXISTING SudoVDA output by GDI name / target id — do NOT create one (the host owns
// the virtual output + its isolate/restore; a second topology owner breaks DDA recovery).