Full project rename, decided 2026-06-10: - Crates/binaries: punktfunk-core / punktfunk-host / punktfunk-client-rs. - C ABI: punktfunk_* symbols, Punktfunk* types, include/punktfunk_core.h, PUNKTFUNK_FEATURE_QUIC guard (header regenerated; cbindgen renames updated, incl. PUNKTFUNK_BTN_*/PUNKTFUNK_AXIS_* wire constants). - Protocol: punktfunk/1 — control-plane magic LMN1 → PKF1, nonce salt lmn1 → pkf1. WIRE BREAK: clients must be rebuilt from this revision. - Env knobs: PUNKTFUNK_VIDEO_SOURCE / PUNKTFUNK_COMPOSITOR / PUNKTFUNK_ZEROCOPY / …. - Host config dir: ~/.config/punktfunk (the box's dir was migrated in place — the persistent identity is unchanged, pinned fingerprints stay valid). - Swift package: PunktfunkKit + PunktfunkCore.xcframework + PunktfunkConnection (Sources/PunktfunkClient app + tests renamed with it); build-xcframework.sh updated. - scripts/: 60-punktfunk.rules, punktfunk-host.service; OpenAPI doc regenerated. Also: scripts/headless/run-headless-kde.sh — full headless Plasma bringup. Root cause of "desktop but no apps/settings" over the stream: plasmashell launched without XDG_MENU_PREFIX=plasma-, so the launcher resolved a nonexistent applications.menu and rendered an empty menu. The script sets the complete KDE session env (menu prefix, KDE_FULL_SESSION, session version) and rebuilds ksycoca before starting plasmashell. Gate: 97/97 tests, clippy -D warnings (both feature sets), fmt, C-ABI harness PASS, zero lumen references left outside .git. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,56 +0,0 @@
|
||||
language = "C"
|
||||
pragma_once = true
|
||||
include_guard = "LUMEN_CORE_H"
|
||||
autogen_warning = "/* Generated by cbindgen from lumen-core. Do not edit by hand. */"
|
||||
header = "/* lumen-core C ABI — see crates/lumen-core/src/abi.rs */"
|
||||
style = "type"
|
||||
cpp_compat = true
|
||||
tab_width = 4
|
||||
documentation = true
|
||||
documentation_style = "c99"
|
||||
|
||||
[parse]
|
||||
parse_deps = false
|
||||
|
||||
[export.rename]
|
||||
"InputEvent" = "LumenInputEvent"
|
||||
"InputKind" = "LumenInputKind"
|
||||
# Gamepad wire constants: bare BTN_* names collide with <linux/input-event-codes.h> (at
|
||||
# DIFFERENT values — last definition silently wins); prefix everything we export.
|
||||
"BTN_DPAD_UP" = "LUMEN_BTN_DPAD_UP"
|
||||
"BTN_DPAD_DOWN" = "LUMEN_BTN_DPAD_DOWN"
|
||||
"BTN_DPAD_LEFT" = "LUMEN_BTN_DPAD_LEFT"
|
||||
"BTN_DPAD_RIGHT" = "LUMEN_BTN_DPAD_RIGHT"
|
||||
"BTN_START" = "LUMEN_BTN_START"
|
||||
"BTN_BACK" = "LUMEN_BTN_BACK"
|
||||
"BTN_LS_CLICK" = "LUMEN_BTN_LS_CLICK"
|
||||
"BTN_RS_CLICK" = "LUMEN_BTN_RS_CLICK"
|
||||
"BTN_LB" = "LUMEN_BTN_LB"
|
||||
"BTN_RB" = "LUMEN_BTN_RB"
|
||||
"BTN_GUIDE" = "LUMEN_BTN_GUIDE"
|
||||
"BTN_A" = "LUMEN_BTN_A"
|
||||
"BTN_B" = "LUMEN_BTN_B"
|
||||
"BTN_X" = "LUMEN_BTN_X"
|
||||
"BTN_Y" = "LUMEN_BTN_Y"
|
||||
"AXIS_LS_X" = "LUMEN_AXIS_LS_X"
|
||||
"AXIS_LS_Y" = "LUMEN_AXIS_LS_Y"
|
||||
"AXIS_RS_X" = "LUMEN_AXIS_RS_X"
|
||||
"AXIS_RS_Y" = "LUMEN_AXIS_RS_Y"
|
||||
"AXIS_LT" = "LUMEN_AXIS_LT"
|
||||
"AXIS_RT" = "LUMEN_AXIS_RT"
|
||||
"AUDIO_MAGIC" = "LUMEN_AUDIO_MAGIC"
|
||||
"RUMBLE_MAGIC" = "LUMEN_RUMBLE_MAGIC"
|
||||
|
||||
# QualifiedScreamingSnakeCase already qualifies each variant with the enum name
|
||||
# (LumenStatus::Ok -> LUMEN_STATUS_OK); do NOT also set prefix_with_name or it doubles.
|
||||
[enum]
|
||||
rename_variants = "QualifiedScreamingSnakeCase"
|
||||
|
||||
[fn]
|
||||
sort_by = "None"
|
||||
|
||||
[struct]
|
||||
derive_eq = false
|
||||
|
||||
[defines]
|
||||
"feature = quic" = "LUMEN_FEATURE_QUIC"
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lumen-client-rs"
|
||||
description = "lumen reference client (M4): VAAPI decode + wgpu/Vulkan present"
|
||||
name = "punktfunk-client-rs"
|
||||
description = "punktfunk reference client (M4): VAAPI decode + wgpu/Vulkan present"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
@@ -9,7 +9,7 @@ authors.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
lumen-core = { path = "../lumen-core", features = ["quic"] }
|
||||
punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
|
||||
quinn = "0.11"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "net", "time", "macros"] }
|
||||
anyhow = "1"
|
||||
@@ -1,4 +1,4 @@
|
||||
//! `lumen-client-rs` — the reference client for `lumen/1` (M3): QUIC control plane, UDP data
|
||||
//! `punktfunk-client-rs` — the reference client for `punktfunk/1` (M3): QUIC control plane, UDP data
|
||||
//! plane, input over QUIC datagrams. Two modes, decided by the host's Welcome:
|
||||
//!
|
||||
//! * **verification** (`frames > 0`, synthetic host): byte-checks deterministic test frames;
|
||||
@@ -14,15 +14,15 @@
|
||||
//! Host→client datagrams (Opus audio, rumble) are counted and reported with the stream
|
||||
//! stats — decode/playback is the platform clients' job.
|
||||
//!
|
||||
//! Usage: `lumen-client-rs [--connect HOST:PORT] [--mode WxHxFPS] [--out FILE] [--input-test]
|
||||
//! Usage: `punktfunk-client-rs [--connect HOST:PORT] [--mode WxHxFPS] [--out FILE] [--input-test]
|
||||
//! [--pin HEX]` (M4 adds VAAPI decode + wgpu present on this same skeleton.)
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use lumen_core::config::Role;
|
||||
use lumen_core::input::{InputEvent, InputKind};
|
||||
use lumen_core::quic::{endpoint, io, Hello, Start, Welcome};
|
||||
use lumen_core::transport::UdpTransport;
|
||||
use lumen_core::{LumenError, Mode, Session};
|
||||
use punktfunk_core::config::Role;
|
||||
use punktfunk_core::input::{InputEvent, InputKind};
|
||||
use punktfunk_core::quic::{endpoint, io, Hello, Start, Welcome};
|
||||
use punktfunk_core::transport::UdpTransport;
|
||||
use punktfunk_core::{Mode, PunktfunkError, Session};
|
||||
use std::io::Write;
|
||||
|
||||
struct Args {
|
||||
@@ -126,25 +126,25 @@ async fn session(args: Args) -> Result<()> {
|
||||
let (ep, observed) = endpoint::client_pinned(args.pin);
|
||||
let ep = ep.map_err(|e| anyhow!("QUIC client endpoint: {e}"))?;
|
||||
let conn = ep
|
||||
.connect(remote, "lumen")
|
||||
.connect(remote, "punktfunk")
|
||||
.context("connect")?
|
||||
.await
|
||||
.context("QUIC handshake (a pin mismatch fails here)")?;
|
||||
match (args.pin, *observed.lock().unwrap()) {
|
||||
(Some(_), _) => tracing::info!(%remote, "lumen/1 connected — host fingerprint pinned"),
|
||||
(Some(_), _) => tracing::info!(%remote, "punktfunk/1 connected — host fingerprint pinned"),
|
||||
(None, Some(fp)) => tracing::info!(
|
||||
%remote,
|
||||
fingerprint = %hex(&fp),
|
||||
"lumen/1 connected (trust-on-first-use) — pass --pin to verify this host"
|
||||
"punktfunk/1 connected (trust-on-first-use) — pass --pin to verify this host"
|
||||
),
|
||||
(None, None) => tracing::info!(%remote, "lumen/1 connected"),
|
||||
(None, None) => tracing::info!(%remote, "punktfunk/1 connected"),
|
||||
}
|
||||
let (mut send, mut recv) = conn.open_bi().await.context("open control stream")?;
|
||||
|
||||
io::write_msg(
|
||||
&mut send,
|
||||
&Hello {
|
||||
abi_version: lumen_core::ABI_VERSION,
|
||||
abi_version: punktfunk_core::ABI_VERSION,
|
||||
mode: args.mode,
|
||||
}
|
||||
.encode(),
|
||||
@@ -210,7 +210,7 @@ async fn session(args: Args) -> Result<()> {
|
||||
}
|
||||
// Gamepad plane: tap A + sweep the left stick on pad 0 (the host
|
||||
// accumulates these into its virtual xpad; needs /dev/uinput access).
|
||||
use lumen_core::input::gamepad::{AXIS_LS_X, BTN_A};
|
||||
use punktfunk_core::input::gamepad::{AXIS_LS_X, BTN_A};
|
||||
let pad_events = [
|
||||
(InputKind::GamepadButton, BTN_A, 1),
|
||||
(InputKind::GamepadButton, BTN_A, 0),
|
||||
@@ -260,10 +260,10 @@ async fn session(args: Args) -> Result<()> {
|
||||
tokio::spawn(async move {
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
while let Ok(d) = conn2.read_datagram().await {
|
||||
if let Some((_, _, opus)) = lumen_core::quic::decode_audio_datagram(&d) {
|
||||
if let Some((_, _, opus)) = punktfunk_core::quic::decode_audio_datagram(&d) {
|
||||
a.fetch_add(1, Relaxed);
|
||||
ab.fetch_add(opus.len() as u64, Relaxed);
|
||||
} else if lumen_core::quic::decode_rumble_datagram(&d).is_some() {
|
||||
} else if punktfunk_core::quic::decode_rumble_datagram(&d).is_some() {
|
||||
r.fetch_add(1, Relaxed);
|
||||
}
|
||||
}
|
||||
@@ -333,7 +333,7 @@ async fn session(args: Args) -> Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(LumenError::NoFrame) => {
|
||||
Err(PunktfunkError::NoFrame) => {
|
||||
std::thread::sleep(std::time::Duration::from_micros(300));
|
||||
}
|
||||
Err(e) => return Err(anyhow!("poll_frame: {e:?}")),
|
||||
@@ -359,7 +359,7 @@ async fn session(args: Args) -> Result<()> {
|
||||
lat_p95_us = pct(0.95),
|
||||
lat_p99_us = pct(0.99),
|
||||
lat_max_us = latencies_us.last().copied().unwrap_or(0),
|
||||
"lumen/1 stream complete (capture→reassembled latency, same-host clock)"
|
||||
"punktfunk/1 stream complete (capture→reassembled latency, same-host clock)"
|
||||
);
|
||||
if expected > 0 {
|
||||
anyhow::ensure!(mismatched == 0, "{mismatched} corrupted frames");
|
||||
@@ -394,7 +394,7 @@ async fn session(args: Args) -> Result<()> {
|
||||
result
|
||||
}
|
||||
|
||||
/// The host's deterministic test frame (mirror of `lumen-host::m3::test_frame`).
|
||||
/// The host's deterministic test frame (mirror of `punktfunk-host::m3::test_frame`).
|
||||
fn test_frame(idx: u32, len: usize) -> Vec<u8> {
|
||||
let mut d = vec![0u8; len];
|
||||
if len >= 4 {
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lumen-core"
|
||||
description = "lumen shared protocol/transport/FEC core, exposed over a stable C ABI"
|
||||
name = "punktfunk-core"
|
||||
description = "punktfunk shared protocol/transport/FEC core, exposed over a stable C ABI"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
@@ -9,10 +9,10 @@ authors.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "lumen_core"
|
||||
# `lib` — so lumen-host / lumen-client-rs / tools link it as a normal Rust crate.
|
||||
# `staticlib` — `liblumen_core.a` for the C test harness and static embedding.
|
||||
# `cdylib` — `liblumen_core.{so,dylib}` for Swift/Kotlin clients via the C ABI.
|
||||
name = "punktfunk_core"
|
||||
# `lib` — so punktfunk-host / punktfunk-client-rs / tools link it as a normal Rust crate.
|
||||
# `staticlib` — `libpunktfunk_core.a` for the C test harness and static embedding.
|
||||
# `cdylib` — `libpunktfunk_core.{so,dylib}` for Swift/Kotlin clients via the C ABI.
|
||||
crate-type = ["lib", "cdylib", "staticlib"]
|
||||
|
||||
[features]
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Generate the C header (`include/lumen_core.h`) from the `extern "C"` surface.
|
||||
//! Generate the C header (`include/punktfunk_core.h`) from the `extern "C"` surface.
|
||||
//!
|
||||
//! cbindgen failure is a warning, not a hard error, so the crate still builds in minimal
|
||||
//! environments (e.g. a CI image without the full toolchain); the header is checked in.
|
||||
@@ -15,20 +15,20 @@ fn main() {
|
||||
println!("cargo:rerun-if-changed=cbindgen.toml");
|
||||
|
||||
let crate_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
|
||||
// Workspace-level include/ dir: crates/lumen-core/ -> ../../include/
|
||||
// Workspace-level include/ dir: crates/punktfunk-core/ -> ../../include/
|
||||
let out = PathBuf::from(&crate_dir)
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("include")
|
||||
.join("lumen_core.h");
|
||||
.join("punktfunk_core.h");
|
||||
|
||||
match cbindgen::generate(&crate_dir) {
|
||||
Ok(bindings) => {
|
||||
bindings.write_to_file(&out);
|
||||
println!("cargo:warning=lumen-core: wrote {}", out.display());
|
||||
println!("cargo:warning=punktfunk-core: wrote {}", out.display());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("cargo:warning=lumen-core: cbindgen failed ({e}); header not regenerated");
|
||||
println!("cargo:warning=punktfunk-core: cbindgen failed ({e}); header not regenerated");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
language = "C"
|
||||
pragma_once = true
|
||||
include_guard = "PUNKTFUNK_CORE_H"
|
||||
autogen_warning = "/* Generated by cbindgen from punktfunk-core. Do not edit by hand. */"
|
||||
header = "/* punktfunk-core C ABI — see crates/punktfunk-core/src/abi.rs */"
|
||||
style = "type"
|
||||
cpp_compat = true
|
||||
tab_width = 4
|
||||
documentation = true
|
||||
documentation_style = "c99"
|
||||
|
||||
[parse]
|
||||
parse_deps = false
|
||||
|
||||
[export.rename]
|
||||
"InputEvent" = "PunktfunkInputEvent"
|
||||
"InputKind" = "PunktfunkInputKind"
|
||||
# Gamepad wire constants: bare BTN_* names collide with <linux/input-event-codes.h> (at
|
||||
# DIFFERENT values — last definition silently wins); prefix everything we export.
|
||||
"BTN_DPAD_UP" = "PUNKTFUNK_BTN_DPAD_UP"
|
||||
"BTN_DPAD_DOWN" = "PUNKTFUNK_BTN_DPAD_DOWN"
|
||||
"BTN_DPAD_LEFT" = "PUNKTFUNK_BTN_DPAD_LEFT"
|
||||
"BTN_DPAD_RIGHT" = "PUNKTFUNK_BTN_DPAD_RIGHT"
|
||||
"BTN_START" = "PUNKTFUNK_BTN_START"
|
||||
"BTN_BACK" = "PUNKTFUNK_BTN_BACK"
|
||||
"BTN_LS_CLICK" = "PUNKTFUNK_BTN_LS_CLICK"
|
||||
"BTN_RS_CLICK" = "PUNKTFUNK_BTN_RS_CLICK"
|
||||
"BTN_LB" = "PUNKTFUNK_BTN_LB"
|
||||
"BTN_RB" = "PUNKTFUNK_BTN_RB"
|
||||
"BTN_GUIDE" = "PUNKTFUNK_BTN_GUIDE"
|
||||
"BTN_A" = "PUNKTFUNK_BTN_A"
|
||||
"BTN_B" = "PUNKTFUNK_BTN_B"
|
||||
"BTN_X" = "PUNKTFUNK_BTN_X"
|
||||
"BTN_Y" = "PUNKTFUNK_BTN_Y"
|
||||
"AXIS_LS_X" = "PUNKTFUNK_AXIS_LS_X"
|
||||
"AXIS_LS_Y" = "PUNKTFUNK_AXIS_LS_Y"
|
||||
"AXIS_RS_X" = "PUNKTFUNK_AXIS_RS_X"
|
||||
"AXIS_RS_Y" = "PUNKTFUNK_AXIS_RS_Y"
|
||||
"AXIS_LT" = "PUNKTFUNK_AXIS_LT"
|
||||
"AXIS_RT" = "PUNKTFUNK_AXIS_RT"
|
||||
"AUDIO_MAGIC" = "PUNKTFUNK_AUDIO_MAGIC"
|
||||
"RUMBLE_MAGIC" = "PUNKTFUNK_RUMBLE_MAGIC"
|
||||
|
||||
# QualifiedScreamingSnakeCase already qualifies each variant with the enum name
|
||||
# (PunktfunkStatus::Ok -> PUNKTFUNK_STATUS_OK); do NOT also set prefix_with_name or it doubles.
|
||||
[enum]
|
||||
rename_variants = "QualifiedScreamingSnakeCase"
|
||||
|
||||
[fn]
|
||||
sort_by = "None"
|
||||
|
||||
[struct]
|
||||
derive_eq = false
|
||||
|
||||
[defines]
|
||||
"feature = quic" = "PUNKTFUNK_FEATURE_QUIC"
|
||||
@@ -1,17 +1,17 @@
|
||||
//! The stable `extern "C"` surface. `cbindgen` turns this module into
|
||||
//! `include/lumen_core.h` (see `build.rs`).
|
||||
//! `include/punktfunk_core.h` (see `build.rs`).
|
||||
//!
|
||||
//! ## Principles (plan §5)
|
||||
//! - Opaque handles only: C sees `LumenSession*`, never a Rust type's fields.
|
||||
//! - Opaque handles only: C sees `PunktfunkSession*`, never a Rust type's fields.
|
||||
//! - All cross-boundary structs are `#[repr(C)]`; buffers are pointer + length.
|
||||
//! - Explicit ownership: every handle from `*_new` / `*_pair` must be passed to
|
||||
//! [`lumen_session_free`]. A [`LumenFrame`]'s `data` is borrowed until the next
|
||||
//! [`punktfunk_session_free`]. A [`PunktfunkFrame`]'s `data` is borrowed until the next
|
||||
//! `poll`/`free` on that session — copy it out before then.
|
||||
//! - Versioned: [`lumen_abi_version`] + `LumenConfig::struct_size` for forward-compat.
|
||||
//! - Versioned: [`punktfunk_abi_version`] + `PunktfunkConfig::struct_size` for forward-compat.
|
||||
//! - Panics never cross the boundary: every entry point is wrapped in `catch_unwind`.
|
||||
|
||||
use crate::config::{Config, FecConfig, FecScheme, ProtocolPhase, Role};
|
||||
use crate::error::LumenStatus;
|
||||
use crate::error::PunktfunkStatus;
|
||||
use crate::input::InputEvent;
|
||||
use crate::session::Session;
|
||||
use crate::stats::Stats;
|
||||
@@ -22,23 +22,23 @@ use std::panic::AssertUnwindSafe;
|
||||
use std::ptr;
|
||||
|
||||
/// Opaque session handle. Pointer-only from C.
|
||||
pub struct LumenSession {
|
||||
pub struct PunktfunkSession {
|
||||
inner: Session,
|
||||
/// Keeps the most recently polled frame alive so [`LumenFrame::data`] stays valid
|
||||
/// Keeps the most recently polled frame alive so [`PunktfunkFrame::data`] stays valid
|
||||
/// until the next poll or free.
|
||||
last_frame: Option<crate::session::Frame>,
|
||||
input_cb: Option<(LumenInputCb, *mut c_void)>,
|
||||
input_cb: Option<(PunktfunkInputCb, *mut c_void)>,
|
||||
}
|
||||
|
||||
/// Forward-compatible session configuration. The caller MUST set `struct_size` to
|
||||
/// `sizeof(LumenConfig)`; the core uses it to detect ABI skew.
|
||||
/// `sizeof(PunktfunkConfig)`; the core uses it to detect ABI skew.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct LumenConfig {
|
||||
pub struct PunktfunkConfig {
|
||||
pub struct_size: u32,
|
||||
/// 0 = host, 1 = client.
|
||||
pub role: u32,
|
||||
/// 1 = P1 (GameStream-compatible), 2 = P2 (`lumen/1`).
|
||||
/// 1 = P1 (GameStream-compatible), 2 = P2 (`punktfunk/1`).
|
||||
pub phase: u32,
|
||||
/// 0 = GF(2⁸), 1 = GF(2¹⁶).
|
||||
pub fec_scheme: u32,
|
||||
@@ -55,27 +55,28 @@ pub struct LumenConfig {
|
||||
pub max_frame_bytes: u64,
|
||||
}
|
||||
|
||||
impl LumenConfig {
|
||||
fn to_config(self) -> Result<Config, LumenStatus> {
|
||||
impl PunktfunkConfig {
|
||||
fn to_config(self) -> Result<Config, PunktfunkStatus> {
|
||||
let role = match self.role {
|
||||
0 => Role::Host,
|
||||
1 => Role::Client,
|
||||
_ => return Err(LumenStatus::InvalidArg),
|
||||
_ => return Err(PunktfunkStatus::InvalidArg),
|
||||
};
|
||||
let phase = match self.phase {
|
||||
1 => ProtocolPhase::P1GameStream,
|
||||
2 => ProtocolPhase::P2Lumen,
|
||||
_ => return Err(LumenStatus::InvalidArg),
|
||||
2 => ProtocolPhase::P2Punktfunk,
|
||||
_ => return Err(PunktfunkStatus::InvalidArg),
|
||||
};
|
||||
// Range-check before narrowing: a `300` fec_percent or `65600` block size must be
|
||||
// rejected, not silently truncated to a valid-looking value.
|
||||
let scheme = u8::try_from(self.fec_scheme)
|
||||
.ok()
|
||||
.and_then(FecScheme::from_u8)
|
||||
.ok_or(LumenStatus::InvalidArg)?;
|
||||
let fec_percent = u8::try_from(self.fec_percent).map_err(|_| LumenStatus::InvalidArg)?;
|
||||
.ok_or(PunktfunkStatus::InvalidArg)?;
|
||||
let fec_percent =
|
||||
u8::try_from(self.fec_percent).map_err(|_| PunktfunkStatus::InvalidArg)?;
|
||||
let max_data_per_block =
|
||||
u16::try_from(self.max_data_per_block).map_err(|_| LumenStatus::InvalidArg)?;
|
||||
u16::try_from(self.max_data_per_block).map_err(|_| PunktfunkStatus::InvalidArg)?;
|
||||
let cfg = Config {
|
||||
role,
|
||||
phase,
|
||||
@@ -96,28 +97,28 @@ impl LumenConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a `LumenConfig` from a caller pointer, enforcing the `struct_size` ABI-skew
|
||||
/// Read a `PunktfunkConfig` from a caller pointer, enforcing the `struct_size` ABI-skew
|
||||
/// guard *before* reading the whole struct: a caller compiled against a smaller (older)
|
||||
/// layout is rejected rather than causing an out-of-bounds read.
|
||||
///
|
||||
/// # Safety
|
||||
/// `cfg` must either be null or point to at least its own declared `struct_size` bytes.
|
||||
unsafe fn config_from_ptr(cfg: *const LumenConfig) -> Result<Config, LumenStatus> {
|
||||
unsafe fn config_from_ptr(cfg: *const PunktfunkConfig) -> Result<Config, PunktfunkStatus> {
|
||||
if cfg.is_null() {
|
||||
return Err(LumenStatus::NullPointer);
|
||||
return Err(PunktfunkStatus::NullPointer);
|
||||
}
|
||||
// Read only the 4-byte size prefix first to bound the subsequent full read.
|
||||
let declared = unsafe { std::ptr::addr_of!((*cfg).struct_size).read_unaligned() } as usize;
|
||||
if declared < std::mem::size_of::<LumenConfig>() {
|
||||
return Err(LumenStatus::InvalidArg);
|
||||
if declared < std::mem::size_of::<PunktfunkConfig>() {
|
||||
return Err(PunktfunkStatus::InvalidArg);
|
||||
}
|
||||
unsafe { *cfg }.to_config()
|
||||
}
|
||||
|
||||
/// A reassembled access unit. `data`/`len` borrow session-owned memory valid until the
|
||||
/// next `lumen_client_poll_frame`/`lumen_session_free` on the same session.
|
||||
/// next `punktfunk_client_poll_frame`/`punktfunk_session_free` on the same session.
|
||||
#[repr(C)]
|
||||
pub struct LumenFrame {
|
||||
pub struct PunktfunkFrame {
|
||||
pub data: *const u8,
|
||||
pub len: usize,
|
||||
pub frame_index: u32,
|
||||
@@ -128,7 +129,7 @@ pub struct LumenFrame {
|
||||
/// Snapshot of session counters.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct LumenStats {
|
||||
pub struct PunktfunkStats {
|
||||
pub frames_submitted: u64,
|
||||
pub frames_completed: u64,
|
||||
pub frames_dropped: u64,
|
||||
@@ -140,9 +141,9 @@ pub struct LumenStats {
|
||||
pub bytes_received: u64,
|
||||
}
|
||||
|
||||
impl From<Stats> for LumenStats {
|
||||
impl From<Stats> for PunktfunkStats {
|
||||
fn from(s: Stats) -> Self {
|
||||
LumenStats {
|
||||
PunktfunkStats {
|
||||
frames_submitted: s.frames_submitted,
|
||||
frames_completed: s.frames_completed,
|
||||
frames_dropped: s.frames_dropped,
|
||||
@@ -156,16 +157,16 @@ impl From<Stats> for LumenStats {
|
||||
}
|
||||
}
|
||||
|
||||
/// Host-side callback invoked for each input event drained by `lumen_host_poll_input`.
|
||||
pub type LumenInputCb = extern "C" fn(event: *const InputEvent, user: *mut c_void);
|
||||
/// Host-side callback invoked for each input event drained by `punktfunk_host_poll_input`.
|
||||
pub type PunktfunkInputCb = extern "C" fn(event: *const InputEvent, user: *mut c_void);
|
||||
|
||||
#[inline]
|
||||
fn guard<F: FnOnce() -> LumenStatus>(f: F) -> LumenStatus {
|
||||
std::panic::catch_unwind(AssertUnwindSafe(f)).unwrap_or(LumenStatus::Panic)
|
||||
fn guard<F: FnOnce() -> PunktfunkStatus>(f: F) -> PunktfunkStatus {
|
||||
std::panic::catch_unwind(AssertUnwindSafe(f)).unwrap_or(PunktfunkStatus::Panic)
|
||||
}
|
||||
|
||||
fn new_handle(session: Session) -> *mut LumenSession {
|
||||
Box::into_raw(Box::new(LumenSession {
|
||||
fn new_handle(session: Session) -> *mut PunktfunkSession {
|
||||
Box::into_raw(Box::new(PunktfunkSession {
|
||||
inner: session,
|
||||
last_frame: None,
|
||||
input_cb: None,
|
||||
@@ -174,7 +175,7 @@ fn new_handle(session: Session) -> *mut LumenSession {
|
||||
|
||||
/// Current ABI version. Mismatch with [`crate::ABI_VERSION`] means incompatible core.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn lumen_abi_version() -> u32 {
|
||||
pub extern "C" fn punktfunk_abi_version() -> u32 {
|
||||
crate::ABI_VERSION
|
||||
}
|
||||
|
||||
@@ -184,11 +185,11 @@ pub extern "C" fn lumen_abi_version() -> u32 {
|
||||
/// # Safety
|
||||
/// `cfg`, `local`, `peer` must be valid pointers; the strings must be NUL-terminated.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn lumen_session_new(
|
||||
cfg: *const LumenConfig,
|
||||
pub unsafe extern "C" fn punktfunk_session_new(
|
||||
cfg: *const PunktfunkConfig,
|
||||
local: *const c_char,
|
||||
peer: *const c_char,
|
||||
) -> *mut LumenSession {
|
||||
) -> *mut PunktfunkSession {
|
||||
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
if cfg.is_null() || local.is_null() || peer.is_null() {
|
||||
return ptr::null_mut();
|
||||
@@ -223,16 +224,16 @@ pub unsafe extern "C" fn lumen_session_new(
|
||||
/// # Safety
|
||||
/// All four pointers must be valid; the two out-params receive owned handles.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn lumen_test_loopback_pair(
|
||||
host_cfg: *const LumenConfig,
|
||||
client_cfg: *const LumenConfig,
|
||||
out_host: *mut *mut LumenSession,
|
||||
out_client: *mut *mut LumenSession,
|
||||
) -> LumenStatus {
|
||||
pub unsafe extern "C" fn punktfunk_test_loopback_pair(
|
||||
host_cfg: *const PunktfunkConfig,
|
||||
client_cfg: *const PunktfunkConfig,
|
||||
out_host: *mut *mut PunktfunkSession,
|
||||
out_client: *mut *mut PunktfunkSession,
|
||||
) -> PunktfunkStatus {
|
||||
guard(|| {
|
||||
if host_cfg.is_null() || client_cfg.is_null() || out_host.is_null() || out_client.is_null()
|
||||
{
|
||||
return LumenStatus::NullPointer;
|
||||
return PunktfunkStatus::NullPointer;
|
||||
}
|
||||
let hconf = match unsafe { config_from_ptr(host_cfg) } {
|
||||
Ok(c) => c,
|
||||
@@ -255,16 +256,16 @@ pub unsafe extern "C" fn lumen_test_loopback_pair(
|
||||
*out_host = new_handle(hs);
|
||||
*out_client = new_handle(cs);
|
||||
}
|
||||
LumenStatus::Ok
|
||||
PunktfunkStatus::Ok
|
||||
})
|
||||
}
|
||||
|
||||
/// Free a session handle. Safe to call with NULL.
|
||||
///
|
||||
/// # Safety
|
||||
/// `s` must be a handle from `lumen_session_new`/`lumen_test_loopback_pair`, freed once.
|
||||
/// `s` must be a handle from `punktfunk_session_new`/`punktfunk_test_loopback_pair`, freed once.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn lumen_session_free(s: *mut LumenSession) {
|
||||
pub unsafe extern "C" fn punktfunk_session_free(s: *mut PunktfunkSession) {
|
||||
if !s.is_null() {
|
||||
drop(unsafe { Box::from_raw(s) });
|
||||
}
|
||||
@@ -275,20 +276,20 @@ pub unsafe extern "C" fn lumen_session_free(s: *mut LumenSession) {
|
||||
/// # Safety
|
||||
/// `s` is a valid host handle; `data` points to `len` readable bytes (or `len == 0`).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn lumen_host_submit_frame(
|
||||
s: *mut LumenSession,
|
||||
pub unsafe extern "C" fn punktfunk_host_submit_frame(
|
||||
s: *mut PunktfunkSession,
|
||||
data: *const u8,
|
||||
len: usize,
|
||||
pts_ns: u64,
|
||||
flags: u32,
|
||||
) -> LumenStatus {
|
||||
) -> PunktfunkStatus {
|
||||
guard(|| {
|
||||
let s = match unsafe { s.as_mut() } {
|
||||
Some(s) => s,
|
||||
None => return LumenStatus::NullPointer,
|
||||
None => return PunktfunkStatus::NullPointer,
|
||||
};
|
||||
if data.is_null() && len != 0 {
|
||||
return LumenStatus::NullPointer;
|
||||
return PunktfunkStatus::NullPointer;
|
||||
}
|
||||
let slice = if len == 0 {
|
||||
&[][..]
|
||||
@@ -296,36 +297,36 @@ pub unsafe extern "C" fn lumen_host_submit_frame(
|
||||
unsafe { std::slice::from_raw_parts(data, len) }
|
||||
};
|
||||
match s.inner.submit_frame(slice, pts_ns, flags) {
|
||||
Ok(()) => LumenStatus::Ok,
|
||||
Ok(()) => PunktfunkStatus::Ok,
|
||||
Err(e) => e.status(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Client: poll for the next reassembled access unit. Returns [`LumenStatus::NoFrame`]
|
||||
/// Client: poll for the next reassembled access unit. Returns [`PunktfunkStatus::NoFrame`]
|
||||
/// when nothing is ready yet. On `Ok`, `*out` borrows session memory until the next poll.
|
||||
///
|
||||
/// # Safety
|
||||
/// `s` is a valid client handle; `out` points to a writable `LumenFrame`.
|
||||
/// `s` is a valid client handle; `out` points to a writable `PunktfunkFrame`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn lumen_client_poll_frame(
|
||||
s: *mut LumenSession,
|
||||
out: *mut LumenFrame,
|
||||
) -> LumenStatus {
|
||||
pub unsafe extern "C" fn punktfunk_client_poll_frame(
|
||||
s: *mut PunktfunkSession,
|
||||
out: *mut PunktfunkFrame,
|
||||
) -> PunktfunkStatus {
|
||||
guard(|| {
|
||||
let s = match unsafe { s.as_mut() } {
|
||||
Some(s) => s,
|
||||
None => return LumenStatus::NullPointer,
|
||||
None => return PunktfunkStatus::NullPointer,
|
||||
};
|
||||
if out.is_null() {
|
||||
return LumenStatus::NullPointer;
|
||||
return PunktfunkStatus::NullPointer;
|
||||
}
|
||||
match s.inner.poll_frame() {
|
||||
Ok(frame) => {
|
||||
s.last_frame = Some(frame);
|
||||
let f = s.last_frame.as_ref().unwrap();
|
||||
unsafe {
|
||||
*out = LumenFrame {
|
||||
*out = PunktfunkFrame {
|
||||
data: f.data.as_ptr(),
|
||||
len: f.data.len(),
|
||||
frame_index: f.frame_index,
|
||||
@@ -333,7 +334,7 @@ pub unsafe extern "C" fn lumen_client_poll_frame(
|
||||
flags: f.flags,
|
||||
};
|
||||
}
|
||||
LumenStatus::Ok
|
||||
PunktfunkStatus::Ok
|
||||
}
|
||||
Err(e) => e.status(),
|
||||
}
|
||||
@@ -345,60 +346,60 @@ pub unsafe extern "C" fn lumen_client_poll_frame(
|
||||
/// # Safety
|
||||
/// `s` is a valid client handle; `ev` points to a valid [`InputEvent`].
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn lumen_send_input(
|
||||
s: *mut LumenSession,
|
||||
pub unsafe extern "C" fn punktfunk_send_input(
|
||||
s: *mut PunktfunkSession,
|
||||
ev: *const InputEvent,
|
||||
) -> LumenStatus {
|
||||
) -> PunktfunkStatus {
|
||||
guard(|| {
|
||||
let s = match unsafe { s.as_mut() } {
|
||||
Some(s) => s,
|
||||
None => return LumenStatus::NullPointer,
|
||||
None => return PunktfunkStatus::NullPointer,
|
||||
};
|
||||
let ev = match unsafe { ev.as_ref() } {
|
||||
Some(e) => e,
|
||||
None => return LumenStatus::NullPointer,
|
||||
None => return PunktfunkStatus::NullPointer,
|
||||
};
|
||||
match s.inner.send_input(ev) {
|
||||
Ok(()) => LumenStatus::Ok,
|
||||
Ok(()) => PunktfunkStatus::Ok,
|
||||
Err(e) => e.status(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Register the host-side input callback (pass a NULL fn pointer to clear). The callback
|
||||
/// fires from within [`lumen_host_poll_input`], on the calling thread.
|
||||
/// fires from within [`punktfunk_host_poll_input`], on the calling thread.
|
||||
///
|
||||
/// # Safety
|
||||
/// `s` is a valid host handle; `user` is passed back verbatim to `cb`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn lumen_set_input_callback(
|
||||
s: *mut LumenSession,
|
||||
// Written as an explicit `Option<fn>` (not the `LumenInputCb` alias) so cbindgen
|
||||
pub unsafe extern "C" fn punktfunk_set_input_callback(
|
||||
s: *mut PunktfunkSession,
|
||||
// Written as an explicit `Option<fn>` (not the `PunktfunkInputCb` alias) so cbindgen
|
||||
// emits a nullable C function pointer rather than an opaque wrapper struct.
|
||||
cb: Option<extern "C" fn(event: *const InputEvent, user: *mut c_void)>,
|
||||
user: *mut c_void,
|
||||
) -> LumenStatus {
|
||||
) -> PunktfunkStatus {
|
||||
guard(|| {
|
||||
let s = match unsafe { s.as_mut() } {
|
||||
Some(s) => s,
|
||||
None => return LumenStatus::NullPointer,
|
||||
None => return PunktfunkStatus::NullPointer,
|
||||
};
|
||||
s.input_cb = cb.map(|c| (c, user));
|
||||
LumenStatus::Ok
|
||||
PunktfunkStatus::Ok
|
||||
})
|
||||
}
|
||||
|
||||
/// Host: drain all pending input events, invoking the registered callback for each.
|
||||
/// Returns the count dispatched (≥ 0), or a negative [`LumenStatus`] on error.
|
||||
/// Returns the count dispatched (≥ 0), or a negative [`PunktfunkStatus`] on error.
|
||||
///
|
||||
/// # Safety
|
||||
/// `s` is a valid host handle.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn lumen_host_poll_input(s: *mut LumenSession) -> i32 {
|
||||
pub unsafe extern "C" fn punktfunk_host_poll_input(s: *mut PunktfunkSession) -> i32 {
|
||||
let r = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
let s = match unsafe { s.as_mut() } {
|
||||
Some(s) => s,
|
||||
None => return LumenStatus::NullPointer as i32,
|
||||
None => return PunktfunkStatus::NullPointer as i32,
|
||||
};
|
||||
let cb = s.input_cb;
|
||||
let mut count = 0i32;
|
||||
@@ -416,39 +417,39 @@ pub unsafe extern "C" fn lumen_host_poll_input(s: *mut LumenSession) -> i32 {
|
||||
}
|
||||
count
|
||||
}));
|
||||
r.unwrap_or(LumenStatus::Panic as i32)
|
||||
r.unwrap_or(PunktfunkStatus::Panic as i32)
|
||||
}
|
||||
|
||||
/// Copy session counters into `*out`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `s` is a valid handle; `out` points to a writable `LumenStats`.
|
||||
/// `s` is a valid handle; `out` points to a writable `PunktfunkStats`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn lumen_get_stats(
|
||||
s: *mut LumenSession,
|
||||
out: *mut LumenStats,
|
||||
) -> LumenStatus {
|
||||
pub unsafe extern "C" fn punktfunk_get_stats(
|
||||
s: *mut PunktfunkSession,
|
||||
out: *mut PunktfunkStats,
|
||||
) -> PunktfunkStatus {
|
||||
guard(|| {
|
||||
let s = match unsafe { s.as_ref() } {
|
||||
Some(s) => s,
|
||||
None => return LumenStatus::NullPointer,
|
||||
None => return PunktfunkStatus::NullPointer,
|
||||
};
|
||||
if out.is_null() {
|
||||
return LumenStatus::NullPointer;
|
||||
return PunktfunkStatus::NullPointer;
|
||||
}
|
||||
let stats = s.inner.stats();
|
||||
unsafe { *out = LumenStats::from(stats) };
|
||||
LumenStatus::Ok
|
||||
unsafe { *out = PunktfunkStats::from(stats) };
|
||||
PunktfunkStatus::Ok
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// lumen/1 connection API (`quic` feature) — the embeddable client connector platform clients
|
||||
// punktfunk/1 connection API (`quic` feature) — the embeddable client connector platform clients
|
||||
// link (SwiftUI/VideoToolbox, Android, …). In the generated header these are guarded by
|
||||
// `LUMEN_FEATURE_QUIC`; define it when linking a lumen-core built with `--features quic`.
|
||||
// `PUNKTFUNK_FEATURE_QUIC`; define it when linking a punktfunk-core built with `--features quic`.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
/// Opaque handle to a live `lumen/1` connection (QUIC control plane + UDP data plane, all
|
||||
/// Opaque handle to a live `punktfunk/1` connection (QUIC control plane + UDP data plane, all
|
||||
/// pumped on internal threads).
|
||||
///
|
||||
/// Thread contract: each plane (video `next_au`, audio `next_audio`, rumble `next_rumble`)
|
||||
@@ -456,15 +457,15 @@ pub unsafe extern "C" fn lumen_get_stats(
|
||||
/// take shared references internally (per-plane mutexed borrow slots), so cross-plane
|
||||
/// concurrency is sound — never two threads on the *same* plane.
|
||||
#[cfg(feature = "quic")]
|
||||
pub struct LumenConnection {
|
||||
pub struct PunktfunkConnection {
|
||||
inner: crate::client::NativeClient,
|
||||
/// Backs the pointer returned by the last `lumen_connection_next_au` (borrow-until-next-call).
|
||||
/// Backs the pointer returned by the last `punktfunk_connection_next_au` (borrow-until-next-call).
|
||||
last: std::sync::Mutex<Option<crate::session::Frame>>,
|
||||
/// Same, for `lumen_connection_next_audio` (independent of the video slot).
|
||||
/// Same, for `punktfunk_connection_next_audio` (independent of the video slot).
|
||||
last_audio: std::sync::Mutex<Option<crate::client::AudioPacket>>,
|
||||
}
|
||||
|
||||
/// Connect to a `lumen/1` host and start a session at `width`x`height`@`refresh_hz`.
|
||||
/// Connect to a `punktfunk/1` host and start a session at `width`x`height`@`refresh_hz`.
|
||||
/// Blocks up to `timeout_ms` for the handshake. Returns NULL on failure.
|
||||
///
|
||||
/// Trust: `pin_sha256` (NULL or 32 bytes) is the expected SHA-256 fingerprint of the host's
|
||||
@@ -477,7 +478,7 @@ pub struct LumenConnection {
|
||||
/// `pin_sha256`/`observed_sha256_out` are each NULL or valid for 32 bytes.
|
||||
#[cfg(feature = "quic")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn lumen_connect(
|
||||
pub unsafe extern "C" fn punktfunk_connect(
|
||||
host: *const std::os::raw::c_char,
|
||||
port: u16,
|
||||
width: u32,
|
||||
@@ -486,7 +487,7 @@ pub unsafe extern "C" fn lumen_connect(
|
||||
pin_sha256: *const u8,
|
||||
observed_sha256_out: *mut u8,
|
||||
timeout_ms: u32,
|
||||
) -> *mut LumenConnection {
|
||||
) -> *mut PunktfunkConnection {
|
||||
let r = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
if host.is_null() {
|
||||
return std::ptr::null_mut();
|
||||
@@ -521,7 +522,7 @@ pub unsafe extern "C" fn lumen_connect(
|
||||
.copy_from_slice(&c.host_fingerprint);
|
||||
}
|
||||
}
|
||||
Box::into_raw(Box::new(LumenConnection {
|
||||
Box::into_raw(Box::new(PunktfunkConnection {
|
||||
inner: c,
|
||||
last: std::sync::Mutex::new(None),
|
||||
last_audio: std::sync::Mutex::new(None),
|
||||
@@ -534,7 +535,7 @@ pub unsafe extern "C" fn lumen_connect(
|
||||
}
|
||||
|
||||
/// Pull the next reassembled access unit, waiting up to `timeout_ms`. Returns
|
||||
/// [`LumenStatus::NoFrame`] on timeout and [`LumenStatus::Closed`] once the session ended.
|
||||
/// [`PunktfunkStatus::NoFrame`] on timeout and [`PunktfunkStatus::Closed`] once the session ended.
|
||||
/// On `Ok`, `*out` borrows connection memory **until the next `next_au` call** on this
|
||||
/// handle (the audio/rumble planes do not invalidate it).
|
||||
///
|
||||
@@ -543,19 +544,19 @@ pub unsafe extern "C" fn lumen_connect(
|
||||
/// it may run concurrently with one audio-pulling and one rumble-pulling thread.
|
||||
#[cfg(feature = "quic")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn lumen_connection_next_au(
|
||||
c: *mut LumenConnection,
|
||||
out: *mut LumenFrame,
|
||||
pub unsafe extern "C" fn punktfunk_connection_next_au(
|
||||
c: *mut PunktfunkConnection,
|
||||
out: *mut PunktfunkFrame,
|
||||
timeout_ms: u32,
|
||||
) -> LumenStatus {
|
||||
) -> PunktfunkStatus {
|
||||
guard(|| {
|
||||
// Shared reference only: video and audio threads must never alias a `&mut`.
|
||||
let c = match unsafe { c.as_ref() } {
|
||||
Some(c) => c,
|
||||
None => return LumenStatus::NullPointer,
|
||||
None => return PunktfunkStatus::NullPointer,
|
||||
};
|
||||
if out.is_null() {
|
||||
return LumenStatus::NullPointer;
|
||||
return PunktfunkStatus::NullPointer;
|
||||
}
|
||||
match c
|
||||
.inner
|
||||
@@ -566,7 +567,7 @@ pub unsafe extern "C" fn lumen_connection_next_au(
|
||||
*slot = Some(frame);
|
||||
let f = slot.as_ref().unwrap();
|
||||
unsafe {
|
||||
*out = LumenFrame {
|
||||
*out = PunktfunkFrame {
|
||||
data: f.data.as_ptr(),
|
||||
len: f.data.len(),
|
||||
frame_index: f.frame_index,
|
||||
@@ -574,18 +575,18 @@ pub unsafe extern "C" fn lumen_connection_next_au(
|
||||
flags: f.flags,
|
||||
};
|
||||
}
|
||||
LumenStatus::Ok
|
||||
PunktfunkStatus::Ok
|
||||
}
|
||||
Err(e) => e.status(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// One Opus audio packet pulled off a `lumen/1` connection (48 kHz stereo, 5 ms frames).
|
||||
/// `data` borrows connection memory until the next `lumen_connection_next_audio` call.
|
||||
/// One Opus audio packet pulled off a `punktfunk/1` connection (48 kHz stereo, 5 ms frames).
|
||||
/// `data` borrows connection memory until the next `punktfunk_connection_next_audio` call.
|
||||
#[cfg(feature = "quic")]
|
||||
#[repr(C)]
|
||||
pub struct LumenAudioPacket {
|
||||
pub struct PunktfunkAudioPacket {
|
||||
pub data: *const u8,
|
||||
pub len: usize,
|
||||
pub seq: u32,
|
||||
@@ -593,7 +594,7 @@ pub struct LumenAudioPacket {
|
||||
}
|
||||
|
||||
/// Pull the next Opus audio packet, waiting up to `timeout_ms`. Returns
|
||||
/// [`LumenStatus::NoFrame`] on timeout and [`LumenStatus::Closed`] once the session ended.
|
||||
/// [`PunktfunkStatus::NoFrame`] on timeout and [`PunktfunkStatus::Closed`] once the session ended.
|
||||
/// On `Ok`, `out->data` borrows connection memory **until the next audio call** on this
|
||||
/// handle (independent of the video slot). Drain from a dedicated audio thread — packets
|
||||
/// arrive every 5 ms and the internal queue holds 320 ms.
|
||||
@@ -603,18 +604,18 @@ pub struct LumenAudioPacket {
|
||||
/// it may run concurrently with the video/rumble pullers.
|
||||
#[cfg(feature = "quic")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn lumen_connection_next_audio(
|
||||
c: *mut LumenConnection,
|
||||
out: *mut LumenAudioPacket,
|
||||
pub unsafe extern "C" fn punktfunk_connection_next_audio(
|
||||
c: *mut PunktfunkConnection,
|
||||
out: *mut PunktfunkAudioPacket,
|
||||
timeout_ms: u32,
|
||||
) -> LumenStatus {
|
||||
) -> PunktfunkStatus {
|
||||
guard(|| {
|
||||
let c = match unsafe { c.as_ref() } {
|
||||
Some(c) => c,
|
||||
None => return LumenStatus::NullPointer,
|
||||
None => return PunktfunkStatus::NullPointer,
|
||||
};
|
||||
if out.is_null() {
|
||||
return LumenStatus::NullPointer;
|
||||
return PunktfunkStatus::NullPointer;
|
||||
}
|
||||
match c
|
||||
.inner
|
||||
@@ -625,14 +626,14 @@ pub unsafe extern "C" fn lumen_connection_next_audio(
|
||||
*slot = Some(pkt);
|
||||
let p = slot.as_ref().unwrap();
|
||||
unsafe {
|
||||
*out = LumenAudioPacket {
|
||||
*out = PunktfunkAudioPacket {
|
||||
data: p.data.as_ptr(),
|
||||
len: p.data.len(),
|
||||
seq: p.seq,
|
||||
pts_ns: p.pts_ns,
|
||||
};
|
||||
}
|
||||
LumenStatus::Ok
|
||||
PunktfunkStatus::Ok
|
||||
}
|
||||
Err(e) => e.status(),
|
||||
}
|
||||
@@ -641,24 +642,24 @@ pub unsafe extern "C" fn lumen_connection_next_audio(
|
||||
|
||||
/// Pull the next rumble (force-feedback) update, waiting up to `timeout_ms`. Amplitudes
|
||||
/// are 0..0xFFFF (`low` = low-frequency motor, `high` = high-frequency), `(0, 0)` = stop.
|
||||
/// Same timeout/closed semantics as [`lumen_connection_next_audio`].
|
||||
/// Same timeout/closed semantics as [`punktfunk_connection_next_audio`].
|
||||
///
|
||||
/// # Safety
|
||||
/// `c` is a valid connection handle; out pointers are writable (NULLs are skipped). At
|
||||
/// most one thread pulls rumble — it may run concurrently with the video/audio pullers.
|
||||
#[cfg(feature = "quic")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn lumen_connection_next_rumble(
|
||||
c: *mut LumenConnection,
|
||||
pub unsafe extern "C" fn punktfunk_connection_next_rumble(
|
||||
c: *mut PunktfunkConnection,
|
||||
pad: *mut u16,
|
||||
low: *mut u16,
|
||||
high: *mut u16,
|
||||
timeout_ms: u32,
|
||||
) -> LumenStatus {
|
||||
) -> PunktfunkStatus {
|
||||
guard(|| {
|
||||
let c = match unsafe { c.as_ref() } {
|
||||
Some(c) => c,
|
||||
None => return LumenStatus::NullPointer,
|
||||
None => return PunktfunkStatus::NullPointer,
|
||||
};
|
||||
match c
|
||||
.inner
|
||||
@@ -676,7 +677,7 @@ pub unsafe extern "C" fn lumen_connection_next_rumble(
|
||||
*high = h;
|
||||
}
|
||||
}
|
||||
LumenStatus::Ok
|
||||
PunktfunkStatus::Ok
|
||||
}
|
||||
Err(e) => e.status(),
|
||||
}
|
||||
@@ -689,21 +690,21 @@ pub unsafe extern "C" fn lumen_connection_next_rumble(
|
||||
/// `c` is a valid connection handle; `ev` points to a valid [`InputEvent`].
|
||||
#[cfg(feature = "quic")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn lumen_connection_send_input(
|
||||
c: *mut LumenConnection,
|
||||
pub unsafe extern "C" fn punktfunk_connection_send_input(
|
||||
c: *mut PunktfunkConnection,
|
||||
ev: *const InputEvent,
|
||||
) -> LumenStatus {
|
||||
) -> PunktfunkStatus {
|
||||
guard(|| {
|
||||
let c = match unsafe { c.as_ref() } {
|
||||
Some(c) => c,
|
||||
None => return LumenStatus::NullPointer,
|
||||
None => return PunktfunkStatus::NullPointer,
|
||||
};
|
||||
let ev = match unsafe { ev.as_ref() } {
|
||||
Some(e) => e,
|
||||
None => return LumenStatus::NullPointer,
|
||||
None => return PunktfunkStatus::NullPointer,
|
||||
};
|
||||
match c.inner.send_input(ev) {
|
||||
Ok(()) => LumenStatus::Ok,
|
||||
Ok(()) => PunktfunkStatus::Ok,
|
||||
Err(e) => e.status(),
|
||||
}
|
||||
})
|
||||
@@ -715,16 +716,16 @@ pub unsafe extern "C" fn lumen_connection_send_input(
|
||||
/// `c` is a valid connection handle; out pointers are writable (NULLs are skipped).
|
||||
#[cfg(feature = "quic")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn lumen_connection_mode(
|
||||
c: *const LumenConnection,
|
||||
pub unsafe extern "C" fn punktfunk_connection_mode(
|
||||
c: *const PunktfunkConnection,
|
||||
width: *mut u32,
|
||||
height: *mut u32,
|
||||
refresh_hz: *mut u32,
|
||||
) -> LumenStatus {
|
||||
) -> PunktfunkStatus {
|
||||
guard(|| {
|
||||
let c = match unsafe { c.as_ref() } {
|
||||
Some(c) => c,
|
||||
None => return LumenStatus::NullPointer,
|
||||
None => return PunktfunkStatus::NullPointer,
|
||||
};
|
||||
unsafe {
|
||||
if !width.is_null() {
|
||||
@@ -737,17 +738,17 @@ pub unsafe extern "C" fn lumen_connection_mode(
|
||||
*refresh_hz = c.inner.mode.refresh_hz;
|
||||
}
|
||||
}
|
||||
LumenStatus::Ok
|
||||
PunktfunkStatus::Ok
|
||||
})
|
||||
}
|
||||
|
||||
/// Close the connection and free the handle (joins the internal threads). NULL is a no-op.
|
||||
///
|
||||
/// # Safety
|
||||
/// `c` was returned by [`lumen_connect`] and is not used after this call.
|
||||
/// `c` was returned by [`punktfunk_connect`] and is not used after this call.
|
||||
#[cfg(feature = "quic")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn lumen_connection_close(c: *mut LumenConnection) {
|
||||
pub unsafe extern "C" fn punktfunk_connection_close(c: *mut PunktfunkConnection) {
|
||||
if !c.is_null() {
|
||||
drop(unsafe { Box::from_raw(c) });
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
//! The embeddable `lumen/1` client connector (M4 groundwork), behind the `quic` feature.
|
||||
//! The embeddable `punktfunk/1` client connector (M4 groundwork), behind the `quic` feature.
|
||||
//!
|
||||
//! [`NativeClient::connect`] runs the full client side of the protocol — QUIC handshake
|
||||
//! ([`crate::quic`]), UDP data plane ([`crate::session::Session`] on a native thread), input
|
||||
//! datagrams — and hands the embedder a dead-simple surface: *pull reassembled access units,
|
||||
//! push input events*. This is what the platform clients (SwiftUI/VideoToolbox, Android, …)
|
||||
//! link via the C ABI (`lumen_connect` & co. in [`crate::abi`]); `lumen-client-rs` is the
|
||||
//! link via the C ABI (`punktfunk_connect` & co. in [`crate::abi`]); `punktfunk-client-rs` is the
|
||||
//! Rust-native consumer of the same flow.
|
||||
//!
|
||||
//! Threading: one worker thread owns a tokio runtime (QUIC control plane only — design
|
||||
@@ -12,7 +12,7 @@
|
||||
//! channel. All methods are safe to call from any single embedder thread.
|
||||
|
||||
use crate::config::{Mode, Role};
|
||||
use crate::error::{LumenError, Result};
|
||||
use crate::error::{PunktfunkError, Result};
|
||||
use crate::input::InputEvent;
|
||||
use crate::quic::{endpoint, io, Hello, Start, Welcome};
|
||||
use crate::session::{Frame, Session};
|
||||
@@ -60,11 +60,11 @@ pub struct NativeClient {
|
||||
}
|
||||
|
||||
impl NativeClient {
|
||||
/// Connect to a `lumen/1` host and start the session at (up to) `mode`. Blocks until the
|
||||
/// Connect to a `punktfunk/1` host and start the session at (up to) `mode`. Blocks until the
|
||||
/// handshake completes or `timeout` elapses.
|
||||
///
|
||||
/// `pin`: expected SHA-256 of the host's certificate. `Some` and the host presents
|
||||
/// anything else → the handshake is rejected ([`LumenError::Crypto`]). `None` = trust on
|
||||
/// anything else → the handshake is rejected ([`PunktfunkError::Crypto`]). `None` = trust on
|
||||
/// first use; check [`NativeClient::host_fingerprint`] after connecting.
|
||||
pub fn connect(
|
||||
host: &str,
|
||||
@@ -83,7 +83,7 @@ impl NativeClient {
|
||||
let host = host.to_string();
|
||||
let shutdown_w = shutdown.clone();
|
||||
let worker = std::thread::Builder::new()
|
||||
.name("lumen-client".into())
|
||||
.name("punktfunk-client".into())
|
||||
.spawn(move || {
|
||||
let rt = match tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(2)
|
||||
@@ -92,7 +92,7 @@ impl NativeClient {
|
||||
{
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
let _ = ready_tx.send(Err(LumenError::Io(e)));
|
||||
let _ = ready_tx.send(Err(PunktfunkError::Io(e)));
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -109,14 +109,14 @@ impl NativeClient {
|
||||
shutdown: shutdown_w,
|
||||
}));
|
||||
})
|
||||
.map_err(LumenError::Io)?;
|
||||
.map_err(PunktfunkError::Io)?;
|
||||
|
||||
let (negotiated, fingerprint) = match ready_rx.recv_timeout(timeout) {
|
||||
Ok(Ok(t)) => t,
|
||||
Ok(Err(e)) => return Err(e),
|
||||
Err(_) => {
|
||||
shutdown.store(true, Ordering::SeqCst);
|
||||
return Err(LumenError::Timeout);
|
||||
return Err(PunktfunkError::Timeout);
|
||||
}
|
||||
};
|
||||
Ok(NativeClient {
|
||||
@@ -131,8 +131,8 @@ impl NativeClient {
|
||||
})
|
||||
}
|
||||
|
||||
/// Pull the next reassembled, FEC-recovered access unit; [`LumenError::NoFrame`] on
|
||||
/// timeout, [`LumenError::Closed`]-class errors once the session ended.
|
||||
/// Pull the next reassembled, FEC-recovered access unit; [`PunktfunkError::NoFrame`] on
|
||||
/// timeout, [`PunktfunkError::Closed`]-class errors once the session ended.
|
||||
///
|
||||
/// Plane concurrency: each pull method drains its own queue, so video, audio and
|
||||
/// rumble may each be pulled from their own thread — but at most one thread per plane
|
||||
@@ -141,19 +141,19 @@ impl NativeClient {
|
||||
pub fn next_frame(&self, timeout: Duration) -> Result<Frame> {
|
||||
match self.frames.recv_timeout(timeout) {
|
||||
Ok(f) => Ok(f),
|
||||
Err(RecvTimeoutError::Timeout) => Err(LumenError::NoFrame),
|
||||
Err(RecvTimeoutError::Disconnected) => Err(LumenError::Closed),
|
||||
Err(RecvTimeoutError::Timeout) => Err(PunktfunkError::NoFrame),
|
||||
Err(RecvTimeoutError::Disconnected) => Err(PunktfunkError::Closed),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pull the next Opus audio packet; [`LumenError::NoFrame`] on timeout,
|
||||
/// [`LumenError::Closed`] once the session ended. Drain on a dedicated audio thread —
|
||||
/// Pull the next Opus audio packet; [`PunktfunkError::NoFrame`] on timeout,
|
||||
/// [`PunktfunkError::Closed`] once the session ended. Drain on a dedicated audio thread —
|
||||
/// packets arrive every 5 ms.
|
||||
pub fn next_audio(&self, timeout: Duration) -> Result<AudioPacket> {
|
||||
match self.audio.recv_timeout(timeout) {
|
||||
Ok(p) => Ok(p),
|
||||
Err(RecvTimeoutError::Timeout) => Err(LumenError::NoFrame),
|
||||
Err(RecvTimeoutError::Disconnected) => Err(LumenError::Closed),
|
||||
Err(RecvTimeoutError::Timeout) => Err(PunktfunkError::NoFrame),
|
||||
Err(RecvTimeoutError::Disconnected) => Err(PunktfunkError::Closed),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,14 +162,14 @@ impl NativeClient {
|
||||
pub fn next_rumble(&self, timeout: Duration) -> Result<(u16, u16, u16)> {
|
||||
match self.rumble.recv_timeout(timeout) {
|
||||
Ok(r) => Ok(r),
|
||||
Err(RecvTimeoutError::Timeout) => Err(LumenError::NoFrame),
|
||||
Err(RecvTimeoutError::Disconnected) => Err(LumenError::Closed),
|
||||
Err(RecvTimeoutError::Timeout) => Err(PunktfunkError::NoFrame),
|
||||
Err(RecvTimeoutError::Disconnected) => Err(PunktfunkError::Closed),
|
||||
}
|
||||
}
|
||||
|
||||
/// Queue one input event for delivery as a QUIC datagram.
|
||||
pub fn send_input(&self, ev: &InputEvent) -> Result<()> {
|
||||
self.input_tx.send(*ev).map_err(|_| LumenError::Closed)
|
||||
self.input_tx.send(*ev).map_err(|_| PunktfunkError::Closed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,12 +212,12 @@ async fn worker_main(args: WorkerArgs) {
|
||||
let setup = async {
|
||||
let remote: std::net::SocketAddr = format!("{host}:{port}")
|
||||
.parse()
|
||||
.map_err(|_| LumenError::InvalidArg("host:port"))?;
|
||||
.map_err(|_| PunktfunkError::InvalidArg("host:port"))?;
|
||||
let (ep, observed) = endpoint::client_pinned(pin);
|
||||
let ep = ep.map_err(|e| LumenError::Io(std::io::Error::other(e.to_string())))?;
|
||||
let ep = ep.map_err(|e| PunktfunkError::Io(std::io::Error::other(e.to_string())))?;
|
||||
let conn = ep
|
||||
.connect(remote, "lumen")
|
||||
.map_err(|_| LumenError::InvalidArg("connect"))?
|
||||
.connect(remote, "punktfunk")
|
||||
.map_err(|_| PunktfunkError::InvalidArg("connect"))?
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// A pin mismatch surfaces as a TLS failure; report it as a crypto error so
|
||||
@@ -225,16 +225,16 @@ async fn worker_main(args: WorkerArgs) {
|
||||
let fp_mismatch = pin.is_some()
|
||||
&& observed.lock().unwrap().map(|fp| Some(fp) != pin) == Some(true);
|
||||
if fp_mismatch {
|
||||
LumenError::Crypto
|
||||
PunktfunkError::Crypto
|
||||
} else {
|
||||
LumenError::Io(std::io::Error::other(e.to_string()))
|
||||
PunktfunkError::Io(std::io::Error::other(e.to_string()))
|
||||
}
|
||||
})?;
|
||||
let fingerprint = observed.lock().unwrap().unwrap_or([0u8; 32]);
|
||||
let (mut send, mut recv) = conn
|
||||
.open_bi()
|
||||
.await
|
||||
.map_err(|e| LumenError::Io(std::io::Error::other(e.to_string())))?;
|
||||
.map_err(|e| PunktfunkError::Io(std::io::Error::other(e.to_string())))?;
|
||||
|
||||
io::write_msg(
|
||||
&mut send,
|
||||
@@ -264,7 +264,7 @@ async fn worker_main(args: WorkerArgs) {
|
||||
let transport =
|
||||
UdpTransport::connect(&format!("0.0.0.0:{udp_port}"), &host_udp.to_string())?;
|
||||
let session = Session::new(welcome.session_config(Role::Client), Box::new(transport))?;
|
||||
Ok::<_, LumenError>((conn, session, welcome.mode, fingerprint))
|
||||
Ok::<_, PunktfunkError>((conn, session, welcome.mode, fingerprint))
|
||||
};
|
||||
|
||||
let (conn, mut session, negotiated, fingerprint) = match setup.await {
|
||||
@@ -328,7 +328,7 @@ async fn worker_main(args: WorkerArgs) {
|
||||
Ok(frame) => {
|
||||
let _ = frame_tx.try_send(frame);
|
||||
}
|
||||
Err(LumenError::NoFrame) => {
|
||||
Err(PunktfunkError::NoFrame) => {
|
||||
std::thread::sleep(Duration::from_micros(300));
|
||||
}
|
||||
Err(_) => break,
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Session configuration and protocol/FEC parameters.
|
||||
|
||||
use crate::error::{LumenError, Result};
|
||||
use crate::error::{PunktfunkError, Result};
|
||||
use crate::packet::{CRYPTO_OVERHEAD, HEADER_LEN, MAX_DATAGRAM_BYTES};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
@@ -13,12 +13,12 @@ pub enum Role {
|
||||
}
|
||||
|
||||
/// Negotiated protocol generation. P1 is GameStream-compatible (GF(2⁸)); P2 is the
|
||||
/// `lumen/1` extension (GF(2¹⁶), multi-block framing, optional QUIC control).
|
||||
/// `punktfunk/1` extension (GF(2¹⁶), multi-block framing, optional QUIC control).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ProtocolPhase {
|
||||
P1GameStream = 1,
|
||||
P2Lumen = 2,
|
||||
P2Punktfunk = 2,
|
||||
}
|
||||
|
||||
/// Erasure-coding field. Mirrors the on-wire `fec_scheme` tag.
|
||||
@@ -141,38 +141,40 @@ impl Config {
|
||||
/// is what keeps the receive-side parser's allocations bounded.
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
if self.shard_payload == 0 || self.shard_payload % 2 != 0 {
|
||||
return Err(LumenError::InvalidArg("shard_payload must be even and > 0"));
|
||||
return Err(PunktfunkError::InvalidArg(
|
||||
"shard_payload must be even and > 0",
|
||||
));
|
||||
}
|
||||
if self.shard_payload > max_shard_payload() {
|
||||
return Err(LumenError::InvalidArg(
|
||||
return Err(PunktfunkError::InvalidArg(
|
||||
"shard_payload too large to fit a datagram (header + crypto overhead)",
|
||||
));
|
||||
}
|
||||
if self.fec.max_data_per_block == 0 {
|
||||
return Err(LumenError::InvalidArg("max_data_per_block must be > 0"));
|
||||
return Err(PunktfunkError::InvalidArg("max_data_per_block must be > 0"));
|
||||
}
|
||||
// The per-block total (data + recovery) must fit both the field ceiling and the
|
||||
// u16 wire fields.
|
||||
let k = self.fec.max_data_per_block as usize;
|
||||
let total = k + self.fec.recovery_for(k);
|
||||
if total > self.fec.scheme.max_total_shards() {
|
||||
return Err(LumenError::InvalidArg(
|
||||
return Err(PunktfunkError::InvalidArg(
|
||||
"max_data_per_block + recovery exceeds the FEC scheme's shard ceiling",
|
||||
));
|
||||
}
|
||||
if self.max_frame_bytes == 0 {
|
||||
return Err(LumenError::InvalidArg("max_frame_bytes must be > 0"));
|
||||
return Err(PunktfunkError::InvalidArg("max_frame_bytes must be > 0"));
|
||||
}
|
||||
// The frame must not need more FEC blocks than the u16 block-count field allows.
|
||||
let total_data = self.max_frame_bytes.div_ceil(self.shard_payload).max(1);
|
||||
let max_blocks = total_data.div_ceil(k).max(1);
|
||||
if max_blocks > u16::MAX as usize {
|
||||
return Err(LumenError::InvalidArg(
|
||||
return Err(PunktfunkError::InvalidArg(
|
||||
"max_frame_bytes too large for this shard/block configuration (block count overflows u16)",
|
||||
));
|
||||
}
|
||||
if self.encrypt && self.key == [0u8; 16] {
|
||||
return Err(LumenError::InvalidArg(
|
||||
return Err(PunktfunkError::InvalidArg(
|
||||
"encrypt requires a non-zero session key (see crypto nonce-uniqueness contract)",
|
||||
));
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
//! nonce. Note: this layer does not provide anti-replay — see `Session`.
|
||||
|
||||
use crate::config::Role;
|
||||
use crate::error::{LumenError, Result};
|
||||
use crate::error::{PunktfunkError, Result};
|
||||
use aes_gcm::aead::{Aead, KeyInit, Payload};
|
||||
use aes_gcm::{Aes128Gcm, Key, Nonce};
|
||||
|
||||
@@ -57,7 +57,7 @@ impl SessionCrypto {
|
||||
aad: &seq.to_be_bytes(),
|
||||
},
|
||||
)
|
||||
.map_err(|_| LumenError::Crypto)
|
||||
.map_err(|_| PunktfunkError::Crypto)
|
||||
}
|
||||
|
||||
/// Open `ciphertext || tag` for sequence `seq` (also bound as associated data).
|
||||
@@ -71,7 +71,7 @@ impl SessionCrypto {
|
||||
aad: &seq.to_be_bytes(),
|
||||
},
|
||||
)
|
||||
.map_err(|_| LumenError::Crypto)
|
||||
.map_err(|_| PunktfunkError::Crypto)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// The core's internal error type. Crosses the C ABI as a [`LumenStatus`] code.
|
||||
/// The core's internal error type. Crosses the C ABI as a [`PunktfunkStatus`] code.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum LumenError {
|
||||
pub enum PunktfunkError {
|
||||
#[error("invalid argument: {0}")]
|
||||
InvalidArg(&'static str),
|
||||
#[error("fec error: {0}")]
|
||||
@@ -25,13 +25,13 @@ pub enum LumenError {
|
||||
Closed,
|
||||
}
|
||||
|
||||
pub type Result<T> = core::result::Result<T, LumenError>;
|
||||
pub type Result<T> = core::result::Result<T, PunktfunkError>;
|
||||
|
||||
/// Stable C ABI status codes. `Ok` is 0; all errors are negative so callers can
|
||||
/// test `rc < 0`. Do not renumber existing variants — only append.
|
||||
#[repr(i32)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LumenStatus {
|
||||
pub enum PunktfunkStatus {
|
||||
Ok = 0,
|
||||
InvalidArg = -1,
|
||||
Fec = -2,
|
||||
@@ -46,19 +46,19 @@ pub enum LumenStatus {
|
||||
Panic = -99,
|
||||
}
|
||||
|
||||
impl LumenError {
|
||||
impl PunktfunkError {
|
||||
/// Map to the C ABI status code.
|
||||
pub fn status(&self) -> LumenStatus {
|
||||
pub fn status(&self) -> PunktfunkStatus {
|
||||
match self {
|
||||
LumenError::InvalidArg(_) => LumenStatus::InvalidArg,
|
||||
LumenError::Fec(_) => LumenStatus::Fec,
|
||||
LumenError::Crypto => LumenStatus::Crypto,
|
||||
LumenError::BadPacket => LumenStatus::BadPacket,
|
||||
LumenError::NoFrame => LumenStatus::NoFrame,
|
||||
LumenError::Unsupported(_) => LumenStatus::Unsupported,
|
||||
LumenError::Io(_) => LumenStatus::Io,
|
||||
LumenError::Timeout => LumenStatus::Timeout,
|
||||
LumenError::Closed => LumenStatus::Closed,
|
||||
PunktfunkError::InvalidArg(_) => PunktfunkStatus::InvalidArg,
|
||||
PunktfunkError::Fec(_) => PunktfunkStatus::Fec,
|
||||
PunktfunkError::Crypto => PunktfunkStatus::Crypto,
|
||||
PunktfunkError::BadPacket => PunktfunkStatus::BadPacket,
|
||||
PunktfunkError::NoFrame => PunktfunkStatus::NoFrame,
|
||||
PunktfunkError::Unsupported(_) => PunktfunkStatus::Unsupported,
|
||||
PunktfunkError::Io(_) => PunktfunkStatus::Io,
|
||||
PunktfunkError::Timeout => PunktfunkStatus::Timeout,
|
||||
PunktfunkError::Closed => PunktfunkStatus::Closed,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ impl InputKind {
|
||||
}
|
||||
|
||||
/// A single input event. `#[repr(C)]` — shared verbatim with the C ABI as
|
||||
/// `LumenInputEvent`.
|
||||
/// `PunktfunkInputEvent`.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct InputEvent {
|
||||
@@ -1,6 +1,6 @@
|
||||
//! # lumen-core
|
||||
//! # punktfunk-core
|
||||
//!
|
||||
//! The shared protocol / transport / FEC core for the lumen low-latency streaming
|
||||
//! The shared protocol / transport / FEC core for the punktfunk low-latency streaming
|
||||
//! stack. It is compiled exactly once and linked by every host and client — directly
|
||||
//! as a Rust `lib`, or across the [C ABI](crate::abi) by Swift / Kotlin / C clients.
|
||||
//!
|
||||
@@ -15,7 +15,7 @@
|
||||
//! - [`session`] — the host (submit frame → FEC → packetize → seal → send) and client
|
||||
//! (recv → open → reorder → FEC recover → reassemble) state machines.
|
||||
//! - [`transport`] — pluggable packet I/O (in-process loopback for tests; UDP for real).
|
||||
//! - [`abi`] — the `extern "C"` surface and `cbindgen`-generated `lumen_core.h`.
|
||||
//! - [`abi`] — the `extern "C"` surface and `cbindgen`-generated `punktfunk_core.h`.
|
||||
//!
|
||||
//! ## Threading contract
|
||||
//!
|
||||
@@ -40,10 +40,10 @@ pub mod stats;
|
||||
pub mod transport;
|
||||
|
||||
pub use config::{Config, FecConfig, FecScheme, Mode, ProtocolPhase, Role};
|
||||
pub use error::{LumenError, LumenStatus, Result};
|
||||
pub use error::{PunktfunkError, PunktfunkStatus, Result};
|
||||
pub use session::{Frame, Session};
|
||||
pub use stats::Stats;
|
||||
|
||||
/// Bump on any breaking change to the [C ABI](crate::abi). Mirrors
|
||||
/// `lumen_abi_version()` and is checked by clients before use.
|
||||
/// `punktfunk_abi_version()` and is checked by clients before use.
|
||||
pub const ABI_VERSION: u32 = 1;
|
||||
@@ -4,7 +4,7 @@
|
||||
//! ## Wire layout
|
||||
//!
|
||||
//! Each packet is a fixed [`PacketHeader`] followed by one FEC shard's payload. Fields
|
||||
//! are host-endian for now (every target platform is little-endian); the `lumen/1` (P2)
|
||||
//! are host-endian for now (every target platform is little-endian); the `punktfunk/1` (P2)
|
||||
//! spec will pin byte order explicitly when we talk to non-LE peers.
|
||||
//!
|
||||
//! ## GameStream mapping (P1)
|
||||
@@ -16,15 +16,15 @@
|
||||
//! concern (it also needs RTP framing + RTSP), this is the coherent internal format.
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::error::{LumenError, Result};
|
||||
use crate::error::{PunktfunkError, Result};
|
||||
use crate::fec::ErasureCoder;
|
||||
use crate::session::Frame;
|
||||
use crate::stats::StatsCounters;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
|
||||
|
||||
/// Identifies a lumen video packet (vs. an input datagram, see [`crate::input`]).
|
||||
pub const LUMEN_MAGIC: u8 = 0xC9;
|
||||
/// Identifies a punktfunk video packet (vs. an input datagram, see [`crate::input`]).
|
||||
pub const PUNKTFUNK_MAGIC: u8 = 0xC9;
|
||||
|
||||
// Frame flags (mirroring GameStream's FLAG_*).
|
||||
pub const FLAG_PIC: u8 = 0x1;
|
||||
@@ -114,10 +114,10 @@ impl Packetizer {
|
||||
// already rejects configs that could reach these for valid frame sizes; this is
|
||||
// the belt-and-suspenders for a frame larger than the negotiated maximum.
|
||||
if payload > u16::MAX as usize {
|
||||
return Err(LumenError::InvalidArg("shard_payload exceeds u16"));
|
||||
return Err(PunktfunkError::InvalidArg("shard_payload exceeds u16"));
|
||||
}
|
||||
if block_count > u16::MAX as usize {
|
||||
return Err(LumenError::Unsupported(
|
||||
return Err(PunktfunkError::Unsupported(
|
||||
"frame too large: block count exceeds u16",
|
||||
));
|
||||
}
|
||||
@@ -144,7 +144,7 @@ impl Packetizer {
|
||||
let recovery = coder.encode(&data_shards, recovery_count)?;
|
||||
let total_shards = block_data_count + recovery_count;
|
||||
if total_shards > u16::MAX as usize {
|
||||
return Err(LumenError::Unsupported("block shard count exceeds u16"));
|
||||
return Err(PunktfunkError::Unsupported("block shard count exceeds u16"));
|
||||
}
|
||||
|
||||
for shard_index in 0..total_shards {
|
||||
@@ -177,7 +177,7 @@ impl Packetizer {
|
||||
recovery_shards: recovery_count as u16,
|
||||
shard_index: shard_index as u16,
|
||||
shard_bytes: payload as u16,
|
||||
magic: LUMEN_MAGIC,
|
||||
magic: PUNKTFUNK_MAGIC,
|
||||
version: self.version,
|
||||
fec_scheme: coder.scheme() as u8,
|
||||
flags,
|
||||
@@ -309,7 +309,7 @@ impl Reassembler {
|
||||
let drop = |stats: &StatsCounters| {
|
||||
StatsCounters::add(&stats.packets_dropped, 1);
|
||||
};
|
||||
if hdr.magic != LUMEN_MAGIC
|
||||
if hdr.magic != PUNKTFUNK_MAGIC
|
||||
|| shard_bytes != lim.shard_bytes
|
||||
|| pkt.len() < HEADER_LEN + shard_bytes
|
||||
|| data_shards == 0
|
||||
@@ -493,7 +493,7 @@ mod tests {
|
||||
recovery_shards: 0,
|
||||
shard_index: 0,
|
||||
shard_bytes: 16,
|
||||
magic: LUMEN_MAGIC,
|
||||
magic: PUNKTFUNK_MAGIC,
|
||||
version: 1,
|
||||
fec_scheme: 0,
|
||||
flags: FLAG_PIC,
|
||||
@@ -1,6 +1,6 @@
|
||||
//! `lumen/1` — the native control plane (M3), gated behind the `quic` feature.
|
||||
//! `punktfunk/1` — the native control plane (M3), gated behind the `quic` feature.
|
||||
//!
|
||||
//! GameStream is lumen's compatibility layer; this is the start of its own protocol. A QUIC
|
||||
//! GameStream is punktfunk's compatibility layer; this is the start of its own protocol. A QUIC
|
||||
//! connection (quinn, tokio — control plane only, never the per-frame path) carries a
|
||||
//! length-prefixed binary handshake on one bidirectional stream:
|
||||
//!
|
||||
@@ -23,10 +23,10 @@
|
||||
//! All integers little-endian; every message is `u16 length || payload`.
|
||||
|
||||
use crate::config::{Config, FecConfig, FecScheme, Mode, ProtocolPhase, Role};
|
||||
use crate::error::{LumenError, Result};
|
||||
use crate::error::{PunktfunkError, Result};
|
||||
|
||||
/// Protocol magic + version, first bytes of every message payload.
|
||||
pub const MAGIC: &[u8; 4] = b"LMN1";
|
||||
pub const MAGIC: &[u8; 4] = b"PKF1";
|
||||
|
||||
/// `client → host`: open the session, requesting a display mode (the host creates its
|
||||
/// virtual output at exactly this size/refresh — native resolution end to end).
|
||||
@@ -71,7 +71,7 @@ impl Hello {
|
||||
|
||||
pub fn decode(b: &[u8]) -> Result<Hello> {
|
||||
if b.len() < 20 || &b[0..4] != MAGIC {
|
||||
return Err(LumenError::InvalidArg("bad Hello"));
|
||||
return Err(PunktfunkError::InvalidArg("bad Hello"));
|
||||
}
|
||||
let u32at = |o: usize| u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]);
|
||||
Ok(Hello {
|
||||
@@ -113,7 +113,7 @@ impl Welcome {
|
||||
// scheme[22] pct[23] max_data[24..26] shard[26..28] encrypt[28] key[29..45]
|
||||
// salt[45..49] frames[49..53].
|
||||
if b.len() < 53 || &b[0..4] != MAGIC {
|
||||
return Err(LumenError::InvalidArg("bad Welcome"));
|
||||
return Err(PunktfunkError::InvalidArg("bad Welcome"));
|
||||
}
|
||||
let u32at = |o: usize| u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]);
|
||||
let u16at = |o: usize| u16::from_le_bytes([b[o], b[o + 1]]);
|
||||
@@ -169,7 +169,7 @@ impl Start {
|
||||
|
||||
pub fn decode(b: &[u8]) -> Result<Start> {
|
||||
if b.len() < 6 || &b[0..4] != MAGIC {
|
||||
return Err(LumenError::InvalidArg("bad Start"));
|
||||
return Err(PunktfunkError::InvalidArg("bad Start"));
|
||||
}
|
||||
Ok(Start {
|
||||
client_udp_port: u16::from_le_bytes([b[4], b[5]]),
|
||||
@@ -265,7 +265,7 @@ pub mod endpoint {
|
||||
/// Server endpoint with a fresh self-signed certificate (tests/dev — production hosts
|
||||
/// persist an identity and use [`server_with_identity`] so clients can pin it).
|
||||
pub fn server(addr: std::net::SocketAddr) -> anyhow_result::Result<quinn::Endpoint> {
|
||||
let cert = rcgen::generate_simple_self_signed(vec!["lumen".into()])
|
||||
let cert = rcgen::generate_simple_self_signed(vec!["punktfunk".into()])
|
||||
.map_err(|e| anyhow_result::Error::msg(format!("self-signed cert: {e}")))?;
|
||||
let cert_der = rustls::pki_types::CertificateDer::from(cert.cert);
|
||||
let key_der = rustls::pki_types::PrivatePkcs8KeyDer::from(cert.key_pair.serialize_der());
|
||||
@@ -351,7 +351,7 @@ pub mod endpoint {
|
||||
(ep, observed)
|
||||
}
|
||||
|
||||
/// Minimal error plumbing without pulling anyhow into lumen-core's public API.
|
||||
/// Minimal error plumbing without pulling anyhow into punktfunk-core's public API.
|
||||
pub mod anyhow_result {
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
#[derive(Debug)]
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
use crate::config::{Config, Role};
|
||||
use crate::crypto::SessionCrypto;
|
||||
use crate::error::{LumenError, Result};
|
||||
use crate::error::{PunktfunkError, Result};
|
||||
use crate::fec::{coder_for, ErasureCoder};
|
||||
use crate::input::InputEvent;
|
||||
use crate::packet::{Packetizer, Reassembler, ReassemblerLimits};
|
||||
@@ -26,7 +26,7 @@ pub struct Frame {
|
||||
}
|
||||
|
||||
/// One end of a stream. Constructed for a single [`Role`]; calling the other role's
|
||||
/// methods returns [`LumenError::InvalidArg`].
|
||||
/// methods returns [`PunktfunkError::InvalidArg`].
|
||||
///
|
||||
/// Note: the AEAD layer authenticates each datagram but does **not** provide anti-replay.
|
||||
/// Video replays are largely absorbed by the reassembler's per-frame dedup, but replayed
|
||||
@@ -96,7 +96,7 @@ impl Session {
|
||||
match &self.crypto {
|
||||
Some(c) => {
|
||||
if wire.len() < 8 {
|
||||
return Err(LumenError::BadPacket);
|
||||
return Err(PunktfunkError::BadPacket);
|
||||
}
|
||||
let seq = u64::from_be_bytes(wire[..8].try_into().unwrap());
|
||||
c.open(seq, &wire[8..])
|
||||
@@ -110,7 +110,7 @@ impl Session {
|
||||
/// Host: FEC-protect, packetize, seal, and send one encoded access unit.
|
||||
pub fn submit_frame(&mut self, data: &[u8], pts_ns: u64, user_flags: u32) -> Result<()> {
|
||||
if self.config.role != Role::Host {
|
||||
return Err(LumenError::InvalidArg(
|
||||
return Err(PunktfunkError::InvalidArg(
|
||||
"submit_frame called on a client session",
|
||||
));
|
||||
}
|
||||
@@ -130,7 +130,7 @@ impl Session {
|
||||
/// Host: drain one pending input event from the client, if any.
|
||||
pub fn poll_input(&mut self) -> Result<Option<InputEvent>> {
|
||||
if self.config.role != Role::Host {
|
||||
return Err(LumenError::InvalidArg(
|
||||
return Err(PunktfunkError::InvalidArg(
|
||||
"poll_input called on a client session",
|
||||
));
|
||||
}
|
||||
@@ -151,17 +151,17 @@ impl Session {
|
||||
// -- Client path ------------------------------------------------------
|
||||
|
||||
/// Client: drain the transport until a whole access unit is recovered, or no more
|
||||
/// packets are pending ([`LumenError::NoFrame`]).
|
||||
/// packets are pending ([`PunktfunkError::NoFrame`]).
|
||||
pub fn poll_frame(&mut self) -> Result<Frame> {
|
||||
if self.config.role != Role::Client {
|
||||
return Err(LumenError::InvalidArg(
|
||||
return Err(PunktfunkError::InvalidArg(
|
||||
"poll_frame called on a host session",
|
||||
));
|
||||
}
|
||||
loop {
|
||||
let wire = match self.transport.recv()? {
|
||||
Some(w) => w,
|
||||
None => return Err(LumenError::NoFrame),
|
||||
None => return Err(PunktfunkError::NoFrame),
|
||||
};
|
||||
let pkt = match self.open_from_wire(&wire) {
|
||||
Ok(p) => p,
|
||||
@@ -184,7 +184,7 @@ impl Session {
|
||||
/// Client: serialize and send one input event to the host.
|
||||
pub fn send_input(&mut self, event: &InputEvent) -> Result<()> {
|
||||
if self.config.role != Role::Client {
|
||||
return Err(LumenError::InvalidArg(
|
||||
return Err(PunktfunkError::InvalidArg(
|
||||
"send_input called on a host session",
|
||||
));
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
/// Immutable snapshot, copied across the C ABI as `LumenStats`.
|
||||
/// Immutable snapshot, copied across the C ABI as `PunktfunkStats`.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Stats {
|
||||
pub frames_submitted: u64,
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* lumen-core C ABI harness — M1 acceptance.
|
||||
* punktfunk-core C ABI harness — M1 acceptance.
|
||||
*
|
||||
* Proves the core links from C and round-trips encoded access units through the full
|
||||
* packetize -> FEC -> in-process loopback (with deterministic packet loss) -> FEC
|
||||
@@ -7,16 +7,16 @@
|
||||
*
|
||||
* Build/run: see tests/c/run.sh (also driven by `cargo test --test c_abi`).
|
||||
*/
|
||||
#include "lumen_core.h"
|
||||
#include "punktfunk_core.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static LumenConfig make_config(uint32_t role, uint32_t drop_period) {
|
||||
LumenConfig c;
|
||||
static PunktfunkConfig make_config(uint32_t role, uint32_t drop_period) {
|
||||
PunktfunkConfig c;
|
||||
memset(&c, 0, sizeof(c));
|
||||
c.struct_size = (uint32_t)sizeof(LumenConfig);
|
||||
c.struct_size = (uint32_t)sizeof(PunktfunkConfig);
|
||||
c.role = role; /* 0 = host, 1 = client */
|
||||
c.phase = 1; /* P1, GameStream-compatible */
|
||||
c.fec_scheme = 0; /* GF(2^8) */
|
||||
@@ -30,16 +30,16 @@ static LumenConfig make_config(uint32_t role, uint32_t drop_period) {
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("lumen-core C ABI harness (abi_version=%u)\n", lumen_abi_version());
|
||||
printf("punktfunk-core C ABI harness (abi_version=%u)\n", punktfunk_abi_version());
|
||||
|
||||
const uint32_t DROP_PERIOD = 8; /* drop 1 of every 8 packets */
|
||||
LumenConfig host_cfg = make_config(0, DROP_PERIOD);
|
||||
LumenConfig client_cfg = make_config(1, DROP_PERIOD);
|
||||
PunktfunkConfig host_cfg = make_config(0, DROP_PERIOD);
|
||||
PunktfunkConfig client_cfg = make_config(1, DROP_PERIOD);
|
||||
|
||||
LumenSession *host = NULL;
|
||||
LumenSession *client = NULL;
|
||||
LumenStatus rc = lumen_test_loopback_pair(&host_cfg, &client_cfg, &host, &client);
|
||||
if (rc != LUMEN_STATUS_OK || !host || !client) {
|
||||
PunktfunkSession *host = NULL;
|
||||
PunktfunkSession *client = NULL;
|
||||
PunktfunkStatus rc = punktfunk_test_loopback_pair(&host_cfg, &client_cfg, &host, &client);
|
||||
if (rc != PUNKTFUNK_STATUS_OK || !host || !client) {
|
||||
fprintf(stderr, "FAIL: loopback_pair rc=%d\n", (int)rc);
|
||||
return 1;
|
||||
}
|
||||
@@ -55,17 +55,17 @@ int main(void) {
|
||||
buf[i] = (uint8_t)((i * 131u) + (unsigned)f * 17u);
|
||||
}
|
||||
|
||||
rc = lumen_host_submit_frame(host, buf, FRAME_LEN, (uint64_t)f * 1000000u, 0);
|
||||
if (rc != LUMEN_STATUS_OK) {
|
||||
rc = punktfunk_host_submit_frame(host, buf, FRAME_LEN, (uint64_t)f * 1000000u, 0);
|
||||
if (rc != PUNKTFUNK_STATUS_OK) {
|
||||
fprintf(stderr, "FAIL: submit frame %d rc=%d\n", f, (int)rc);
|
||||
failures++;
|
||||
continue;
|
||||
}
|
||||
|
||||
LumenFrame out;
|
||||
PunktfunkFrame out;
|
||||
memset(&out, 0, sizeof(out));
|
||||
rc = lumen_client_poll_frame(client, &out);
|
||||
if (rc != LUMEN_STATUS_OK) {
|
||||
rc = punktfunk_client_poll_frame(client, &out);
|
||||
if (rc != PUNKTFUNK_STATUS_OK) {
|
||||
fprintf(stderr, "FAIL: poll frame %d rc=%d (expected recovery)\n", f, (int)rc);
|
||||
failures++;
|
||||
continue;
|
||||
@@ -82,9 +82,9 @@ int main(void) {
|
||||
}
|
||||
}
|
||||
|
||||
LumenStats st;
|
||||
PunktfunkStats st;
|
||||
memset(&st, 0, sizeof(st));
|
||||
lumen_get_stats(client, &st);
|
||||
punktfunk_get_stats(client, &st);
|
||||
printf("client stats: completed=%llu recovered_shards=%llu dropped_pkts=%llu rx_pkts=%llu\n",
|
||||
(unsigned long long)st.frames_completed,
|
||||
(unsigned long long)st.fec_recovered_shards,
|
||||
@@ -97,8 +97,8 @@ int main(void) {
|
||||
}
|
||||
|
||||
free(buf);
|
||||
lumen_session_free(host);
|
||||
lumen_session_free(client);
|
||||
punktfunk_session_free(host);
|
||||
punktfunk_session_free(client);
|
||||
|
||||
if (failures == 0) {
|
||||
printf("PASS: %d frames round-tripped byte-exact through lossy loopback\n", FRAMES);
|
||||
@@ -1,30 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build lumen-core's staticlib, then compile + link + run the C ABI harness against it.
|
||||
# Build punktfunk-core's staticlib, then compile + link + run the C ABI harness against it.
|
||||
# Proves the core links from C. Works on Linux and macOS (link flags come from rustc).
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ws="$(cd "$here/../../../.." && pwd)" # tests/c -> crates/lumen-core -> crates -> ws
|
||||
ws="$(cd "$here/../../../.." && pwd)" # tests/c -> crates/punktfunk-core -> crates -> ws
|
||||
cd "$ws"
|
||||
|
||||
profile="${1:-debug}"
|
||||
build_flag=""
|
||||
[ "$profile" = "release" ] && build_flag="--release"
|
||||
|
||||
echo ">> building lumen-core staticlib ($profile)"
|
||||
cargo build -p lumen-core $build_flag >/dev/null
|
||||
echo ">> building punktfunk-core staticlib ($profile)"
|
||||
cargo build -p punktfunk-core $build_flag >/dev/null
|
||||
|
||||
staticlib="$ws/target/$profile/liblumen_core.a"
|
||||
staticlib="$ws/target/$profile/libpunktfunk_core.a"
|
||||
header_dir="$ws/include"
|
||||
[ -f "$staticlib" ] || { echo "missing $staticlib"; exit 1; }
|
||||
[ -f "$header_dir/lumen_core.h" ] || { echo "missing generated header"; exit 1; }
|
||||
[ -f "$header_dir/punktfunk_core.h" ] || { echo "missing generated header"; exit 1; }
|
||||
|
||||
# Ask rustc what native libs the staticlib needs to link into a C program.
|
||||
native_libs="$(cargo rustc -p lumen-core --lib --crate-type staticlib $build_flag -- \
|
||||
native_libs="$(cargo rustc -p punktfunk-core --lib --crate-type staticlib $build_flag -- \
|
||||
--print native-static-libs 2>&1 | sed -n 's/.*native-static-libs: //p' | tail -1)"
|
||||
echo ">> native libs: ${native_libs:-<none>}"
|
||||
|
||||
out="$(mktemp -d)/lumen_harness"
|
||||
out="$(mktemp -d)/punktfunk_harness"
|
||||
cc="${CC:-cc}"
|
||||
echo ">> compiling + linking harness"
|
||||
$cc -std=c11 -Wall -Wextra -O2 -I "$header_dir" \
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Runs the C ABI harness under `cargo test`: compiles `tests/c/harness.c`, links it
|
||||
//! against the freshly built `liblumen_core.a`, and asserts it round-trips frames
|
||||
//! against the freshly built `libpunktfunk_core.a`, and asserts it round-trips frames
|
||||
//! through the lossy loopback. The cross-platform canonical path (querying rustc for
|
||||
//! link flags) is `tests/c/run.sh`; this mirrors it so `cargo test` alone covers the
|
||||
//! C boundary.
|
||||
@@ -21,13 +21,13 @@ fn native_libs() -> &'static [&'static str] {
|
||||
}
|
||||
|
||||
fn ensure_staticlib(profile_dir: &Path) -> PathBuf {
|
||||
let staticlib = profile_dir.join("liblumen_core.a");
|
||||
let staticlib = profile_dir.join("libpunktfunk_core.a");
|
||||
if !staticlib.exists() {
|
||||
// `cargo test` doesn't always emit the standalone staticlib; build it. The
|
||||
// outer cargo's build lock is released during test execution, so this is safe.
|
||||
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
|
||||
let _ = Command::new(cargo)
|
||||
.args(["build", "-p", "lumen-core"])
|
||||
.args(["build", "-p", "punktfunk-core"])
|
||||
.status();
|
||||
}
|
||||
staticlib
|
||||
@@ -35,7 +35,7 @@ fn ensure_staticlib(profile_dir: &Path) -> PathBuf {
|
||||
|
||||
#[test]
|
||||
fn c_abi_harness_round_trips() {
|
||||
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")); // crates/lumen-core
|
||||
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")); // crates/punktfunk-core
|
||||
let harness = manifest.join("tests/c/harness.c");
|
||||
let include = manifest.join("../../include");
|
||||
|
||||
@@ -50,16 +50,16 @@ fn c_abi_harness_round_trips() {
|
||||
let staticlib = ensure_staticlib(&profile_dir);
|
||||
assert!(
|
||||
staticlib.exists(),
|
||||
"staticlib not found at {} (run `cargo build -p lumen-core`)",
|
||||
"staticlib not found at {} (run `cargo build -p punktfunk-core`)",
|
||||
staticlib.display()
|
||||
);
|
||||
assert!(
|
||||
include.join("lumen_core.h").exists(),
|
||||
"generated header missing; build lumen-core to regenerate it"
|
||||
include.join("punktfunk_core.h").exists(),
|
||||
"generated header missing; build punktfunk-core to regenerate it"
|
||||
);
|
||||
|
||||
let cc = std::env::var("CC").unwrap_or_else(|_| "cc".into());
|
||||
let out = profile_dir.join("lumen_c_harness");
|
||||
let out = profile_dir.join("punktfunk_c_harness");
|
||||
|
||||
let mut compile = Command::new(&cc);
|
||||
compile
|
||||
@@ -3,19 +3,19 @@
|
||||
//! byte-exact recovery, for both FEC schemes, with and without encryption. Plus
|
||||
//! property tests over the FEC layer's loss patterns.
|
||||
|
||||
use lumen_core::config::{Config, FecConfig, FecScheme, ProtocolPhase, Role};
|
||||
use lumen_core::fec::coder_for;
|
||||
use lumen_core::input::{InputEvent, InputKind};
|
||||
use lumen_core::session::Session;
|
||||
use lumen_core::transport::loopback_pair;
|
||||
use proptest::prelude::*;
|
||||
use punktfunk_core::config::{Config, FecConfig, FecScheme, ProtocolPhase, Role};
|
||||
use punktfunk_core::fec::coder_for;
|
||||
use punktfunk_core::input::{InputEvent, InputKind};
|
||||
use punktfunk_core::session::Session;
|
||||
use punktfunk_core::transport::loopback_pair;
|
||||
|
||||
fn config(role: Role, scheme: FecScheme, encrypt: bool, drop_period: u32) -> Config {
|
||||
Config {
|
||||
role,
|
||||
phase: match scheme {
|
||||
FecScheme::Gf8 => ProtocolPhase::P1GameStream,
|
||||
FecScheme::Gf16 => ProtocolPhase::P2Lumen,
|
||||
FecScheme::Gf16 => ProtocolPhase::P2Punktfunk,
|
||||
},
|
||||
fec: FecConfig {
|
||||
scheme,
|
||||
@@ -38,7 +38,7 @@ fn run_stream(
|
||||
encrypt: bool,
|
||||
drop_period: u32,
|
||||
frames: &[Vec<u8>],
|
||||
) -> lumen_core::Stats {
|
||||
) -> punktfunk_core::Stats {
|
||||
let (host_tp, client_tp) = loopback_pair(drop_period, 0);
|
||||
let mut host = Session::new(
|
||||
config(Role::Host, scheme, encrypt, drop_period),
|
||||
Vendored
Vendored
Vendored
Vendored
Vendored
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lumen-host"
|
||||
description = "lumen Linux streaming host: virtual display, capture, encode, input injection"
|
||||
name = "punktfunk-host"
|
||||
description = "punktfunk Linux streaming host: virtual display, capture, encode, input injection"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
@@ -9,8 +9,8 @@ authors.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
lumen-core = { path = "../lumen-core", features = ["quic"] }
|
||||
# M3 native control plane (the `lumen/1` QUIC handshake; data plane stays native-thread UDP).
|
||||
punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
|
||||
# M3 native control plane (the `punktfunk/1` QUIC handshake; data plane stays native-thread UDP).
|
||||
quinn = "0.11"
|
||||
anyhow = "1"
|
||||
tracing = "0.1"
|
||||
@@ -20,7 +20,7 @@ impl PwAudioCapturer {
|
||||
pub fn open() -> Result<PwAudioCapturer> {
|
||||
let (tx, rx) = sync_channel::<Vec<f32>>(64);
|
||||
thread::Builder::new()
|
||||
.name("lumen-pw-audio".into())
|
||||
.name("punktfunk-pw-audio".into())
|
||||
.spawn(move || {
|
||||
if let Err(e) = pw_thread(tx) {
|
||||
tracing::error!(error = %format!("{e:#}"), "pipewire audio thread failed");
|
||||
@@ -60,7 +60,7 @@ fn pw_thread(tx: std::sync::mpsc::SyncSender<Vec<f32>>) -> Result<()> {
|
||||
|
||||
let stream = pw::stream::StreamBox::new(
|
||||
&core,
|
||||
"lumen-audio",
|
||||
"punktfunk-audio",
|
||||
properties! {
|
||||
*pw::keys::MEDIA_TYPE => "Audio",
|
||||
*pw::keys::MEDIA_CATEGORY => "Capture",
|
||||
@@ -90,7 +90,7 @@ pub trait Capturer: Send {
|
||||
}
|
||||
|
||||
/// A deterministic moving test pattern (BGRx). Lets M0 exercise the encode → file →
|
||||
/// `lumen_core` path with no live capture session, and produces obviously non-static
|
||||
/// `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 {
|
||||
width: u32,
|
||||
@@ -45,7 +45,7 @@ impl PortalCapturer {
|
||||
// Portal handshake (async) on its own thread; hands back the PW fd + node id.
|
||||
let (setup_tx, setup_rx) = std::sync::mpsc::channel::<Result<(OwnedFd, u32), String>>();
|
||||
thread::Builder::new()
|
||||
.name("lumen-portal".into())
|
||||
.name("punktfunk-portal".into())
|
||||
.spawn(move || {
|
||||
if anchored {
|
||||
portal_thread_remote_desktop(setup_tx)
|
||||
@@ -105,7 +105,7 @@ fn spawn_pipewire(
|
||||
let active_cb = active.clone();
|
||||
let zerocopy = crate::zerocopy::enabled();
|
||||
thread::Builder::new()
|
||||
.name("lumen-pipewire".into())
|
||||
.name("punktfunk-pipewire".into())
|
||||
.spawn(move || {
|
||||
if let Err(e) =
|
||||
pipewire::pipewire_thread(fd, node_id, frame_tx, active_cb, zerocopy, preferred)
|
||||
@@ -652,7 +652,7 @@ mod pipewire {
|
||||
|
||||
let stream = pw::stream::StreamBox::new(
|
||||
&core,
|
||||
"lumen-screencast",
|
||||
"punktfunk-screencast",
|
||||
properties! {
|
||||
*pw::keys::MEDIA_TYPE => "Video",
|
||||
*pw::keys::MEDIA_CATEGORY => "Capture",
|
||||
@@ -871,9 +871,9 @@ mod pipewire {
|
||||
.register()
|
||||
.context("register stream listener")?;
|
||||
|
||||
// Debug knob: offer a single fixed format (LUMEN_PW_FIXED_POD="WxH") to bisect
|
||||
// Debug knob: offer a single fixed format (PUNKTFUNK_PW_FIXED_POD="WxH") to bisect
|
||||
// negotiation failures against a producer's exact EnumFormat (e.g. gamescope).
|
||||
let fixed_pod: Option<(u32, u32)> = std::env::var("LUMEN_PW_FIXED_POD")
|
||||
let fixed_pod: Option<(u32, u32)> = std::env::var("PUNKTFUNK_PW_FIXED_POD")
|
||||
.ok()
|
||||
.and_then(|v| v.split_once('x').map(|(w, h)| (w.parse(), h.parse())))
|
||||
.and_then(|(w, h)| Some((w.ok()?, h.ok()?)));
|
||||
@@ -6,7 +6,7 @@
|
||||
use crate::capture::{CapturedFrame, PixelFormat};
|
||||
use anyhow::Result;
|
||||
|
||||
/// An encoded access unit (one NAL/AU) to hand to `lumen_core` for FEC + packetization.
|
||||
/// An encoded access unit (one NAL/AU) to hand to `punktfunk_core` for FEC + packetization.
|
||||
/// `data` is in-band Annex-B (the encoder is opened without a global header), so each
|
||||
/// keyframe carries its own VPS/SPS/PPS — the bytes are both a playable elementary
|
||||
/// stream and a self-contained AU for the wire.
|
||||
@@ -139,7 +139,7 @@ impl NvencEncoder {
|
||||
cuda: bool,
|
||||
) -> Result<Self> {
|
||||
ffmpeg::init().context("ffmpeg init")?;
|
||||
if std::env::var_os("LUMEN_FFMPEG_DEBUG").is_some() {
|
||||
if std::env::var_os("PUNKTFUNK_FFMPEG_DEBUG").is_some() {
|
||||
unsafe { ffi::av_log_set_level(48) }; // AV_LOG_DEBUG — surface NVENC hw-frame rejects
|
||||
}
|
||||
let name = codec.nvenc_name();
|
||||
@@ -198,9 +198,9 @@ impl NvencEncoder {
|
||||
// a single engine's HEVC capacity (~1 Gpix/s); e.g. 5120x1440@240 = 1.77 Gpix/s needs it,
|
||||
// @120 = 0.88 Gpix/s does not. HEVC/AV1 only (not H.264). AUTO won't engage below ~2112px
|
||||
// height, so we force `2`; below the threshold we leave it AUTO (split costs ~2% BD-rate).
|
||||
// Output is standard HEVC — transparent to the client. Override with LUMEN_SPLIT_ENCODE.
|
||||
// Output is standard HEVC — transparent to the client. Override with PUNKTFUNK_SPLIT_ENCODE.
|
||||
let pix_rate = width as u64 * height as u64 * fps as u64;
|
||||
let split = std::env::var("LUMEN_SPLIT_ENCODE").ok();
|
||||
let split = std::env::var("PUNKTFUNK_SPLIT_ENCODE").ok();
|
||||
match split.as_deref() {
|
||||
Some(mode) => opts.set("split_encode_mode", mode),
|
||||
None if matches!(codec, Codec::H265 | Codec::Av1) && pix_rate > 1_000_000_000 => {
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
//! The app catalog: what `/applist` advertises and what `/launch?appid=N` selects. Each entry
|
||||
//! maps to a session recipe — which compositor backend hosts it and (for gamescope) which
|
||||
//! command runs nested. Loaded from `~/.config/lumen/apps.json`; sensible defaults otherwise.
|
||||
//! command runs nested. Loaded from `~/.config/punktfunk/apps.json`; sensible defaults otherwise.
|
||||
//!
|
||||
//! ```json
|
||||
//! [ {"id":1,"title":"Desktop"},
|
||||
@@ -20,7 +20,7 @@ pub struct AppEntry {
|
||||
}
|
||||
|
||||
fn config_path() -> Option<std::path::PathBuf> {
|
||||
Some(std::path::Path::new(&std::env::var("HOME").ok()?).join(".config/lumen/apps.json"))
|
||||
Some(std::path::Path::new(&std::env::var("HOME").ok()?).join(".config/punktfunk/apps.json"))
|
||||
}
|
||||
|
||||
fn parse_compositor(s: &str) -> Option<crate::vdisplay::Compositor> {
|
||||
+3
-3
@@ -38,7 +38,7 @@ pub type AudioCapSlot = Arc<std::sync::Mutex<Option<Box<dyn AudioCapturer>>>>;
|
||||
/// `gcm_key`/`rikeyid` come from `/launch` and key the AES-CBC payload encryption.
|
||||
pub fn start(running: Arc<AtomicBool>, gcm_key: [u8; 16], rikeyid: i32, audio_cap: AudioCapSlot) {
|
||||
let _ = std::thread::Builder::new()
|
||||
.name("lumen-audio".into())
|
||||
.name("punktfunk-audio".into())
|
||||
.spawn(move || {
|
||||
tracing::info!("audio stream starting");
|
||||
if let Err(e) = run(&running, &gcm_key, rikeyid, &audio_cap) {
|
||||
@@ -105,8 +105,8 @@ fn audio_body(
|
||||
// each frame at its 5 ms slot instead. Production is real-time, so the backlog stays small.
|
||||
let start = Instant::now();
|
||||
let mut frame_no: u64 = 0;
|
||||
// Optional linear gain for quiet capture sources (LUMEN_AUDIO_GAIN, default 1.0).
|
||||
let gain: f32 = std::env::var("LUMEN_AUDIO_GAIN")
|
||||
// Optional linear gain for quiet capture sources (PUNKTFUNK_AUDIO_GAIN, default 1.0).
|
||||
let gain: f32 = std::env::var("PUNKTFUNK_AUDIO_GAIN")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(1.0);
|
||||
+2
-2
@@ -38,7 +38,7 @@ impl ServerIdentity {
|
||||
.with_context(|| format!("write {}", cert_path.display()))?;
|
||||
fs::write(&key_path, &k)
|
||||
.with_context(|| format!("write {}", key_path.display()))?;
|
||||
tracing::info!(path = %cert_path.display(), "generated lumen host certificate (RSA-2048)");
|
||||
tracing::info!(path = %cert_path.display(), "generated punktfunk host certificate (RSA-2048)");
|
||||
(c, k)
|
||||
}
|
||||
};
|
||||
@@ -70,7 +70,7 @@ fn generate() -> Result<(String, String)> {
|
||||
let mut params = rcgen::CertificateParams::new(Vec::<String>::new()).context("cert params")?;
|
||||
params
|
||||
.distinguished_name
|
||||
.push(rcgen::DnType::CommonName, "lumen");
|
||||
.push(rcgen::DnType::CommonName, "punktfunk");
|
||||
params.not_before = rcgen::date_time_ymd(2020, 1, 1);
|
||||
params.not_after = rcgen::date_time_ymd(2040, 1, 1);
|
||||
let cert = params.self_signed(&key).context("self-sign cert")?;
|
||||
+2
-2
@@ -51,7 +51,7 @@ pub fn spawn(state: Arc<AppState>) -> Result<()> {
|
||||
tracing::info!(port = CONTROL_PORT, "ENet control listening");
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name("lumen-control".into())
|
||||
.name("punktfunk-control".into())
|
||||
.spawn(move || {
|
||||
// Thread-local (the injector owns non-Send Wayland/xkb state, so it must be
|
||||
// created and live here rather than be captured into the closure).
|
||||
@@ -189,7 +189,7 @@ fn on_receive(
|
||||
|
||||
// Open the injector on demand — by the first input event the compositor session is up.
|
||||
// Backend auto-selects per desktop (wlr on Sway, libei on KWin/GNOME); override with
|
||||
// LUMEN_INPUT_BACKEND.
|
||||
// PUNKTFUNK_INPUT_BACKEND.
|
||||
if injector.is_none() {
|
||||
let backend = crate::inject::default_backend();
|
||||
match crate::inject::open(backend) {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
//! Pairing crypto primitives (control plane only — distinct from `lumen_core`'s AES-GCM
|
||||
//! Pairing crypto primitives (control plane only — distinct from `punktfunk_core`'s AES-GCM
|
||||
//! data-plane sealing). GameStream pairing uses: AES-128-**ECB** with **no padding**,
|
||||
//! SHA-256 (host appversion major ≥ 7), and RSA-PKCS1v15-SHA256 signatures. See the
|
||||
//! `serverinfo + pairing` section of `docs/research/gamestream-protocol-research.json`.
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
//! Decode the GameStream input wire format (carried AES-GCM-encrypted on the ENet control
|
||||
//! stream — see [`super::control`]) into platform-agnostic
|
||||
//! [`lumen_core::input::InputEvent`]s for injection.
|
||||
//! [`punktfunk_core::input::InputEvent`]s for injection.
|
||||
//!
|
||||
//! A decrypted control message is `[u16 type LE][u16 length LE][NV_INPUT packet]`. We only
|
||||
//! handle the input type (`0x0206`); the packet is an 8-byte `NV_INPUT_HEADER` (`size` BE,
|
||||
@@ -9,7 +9,7 @@
|
||||
//! mirror moonlight-common-c `Input.h`; the magic dispatch matches Sunshine `input.cpp`
|
||||
//! (Gen5+, where scroll is `0x0A` and controllers are `0x0C`, so there's no ambiguity).
|
||||
|
||||
use lumen_core::input::{InputEvent, InputKind};
|
||||
use punktfunk_core::input::{InputEvent, InputKind};
|
||||
|
||||
/// Inner control-message type for input (moonlight `packetTypesGen7[IDX_INPUT_DATA]`).
|
||||
const INPUT_DATA_TYPE: u16 = 0x0206;
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
//! 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 `lumen_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/m2-plan.md`.
|
||||
//!
|
||||
//! Status: P1.1 — mDNS `_nvstream._tcp` advertisement + `/serverinfo`. Pairing, RTSP, and
|
||||
//! the media streams follow (see the M2 task list / plan).
|
||||
@@ -154,7 +154,7 @@ pub fn serve(mgmt: crate::mgmt::Options) -> Result<()> {
|
||||
hostname = %state.host.hostname,
|
||||
uniqueid = %state.host.uniqueid,
|
||||
ip = %state.host.local_ip,
|
||||
"lumen GameStream host (P1.1: serverinfo + pairing + mDNS)"
|
||||
"punktfunk GameStream host (P1.1: serverinfo + pairing + mDNS)"
|
||||
);
|
||||
let rt = tokio::runtime::Runtime::new().context("build tokio runtime")?;
|
||||
rt.block_on(async move {
|
||||
@@ -168,13 +168,13 @@ pub fn serve(mgmt: crate::mgmt::Options) -> Result<()> {
|
||||
})
|
||||
}
|
||||
|
||||
/// `~/.config/lumen`, created on demand — host identity + (later) pairing state live here.
|
||||
/// `~/.config/punktfunk`, created on demand — host identity + (later) pairing state live here.
|
||||
fn config_dir() -> PathBuf {
|
||||
let base = std::env::var_os("XDG_CONFIG_HOME")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))
|
||||
.unwrap_or_else(|| PathBuf::from("."));
|
||||
base.join("lumen")
|
||||
base.join("punktfunk")
|
||||
}
|
||||
|
||||
fn hostname_string() -> String {
|
||||
@@ -182,7 +182,7 @@ fn hostname_string() -> String {
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| "lumen-host".to_string())
|
||||
.unwrap_or_else(|| "punktfunk-host".to_string())
|
||||
}
|
||||
|
||||
/// Load the persisted host uniqueid, or mint one (from the kernel UUID source) and store it.
|
||||
@@ -212,7 +212,7 @@ fn primary_local_ip() -> Option<IpAddr> {
|
||||
|
||||
/// Where the paired-client allow-list persists (survives host restarts, like Sunshine).
|
||||
fn paired_path() -> Option<std::path::PathBuf> {
|
||||
Some(std::path::Path::new(&std::env::var("HOME").ok()?).join(".config/lumen/paired.json"))
|
||||
Some(std::path::Path::new(&std::env::var("HOME").ok()?).join(".config/punktfunk/paired.json"))
|
||||
}
|
||||
|
||||
/// Load the persisted paired-client certificate DERs (empty on first run / parse failure).
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
//! The nvhttp servers: plain HTTP on 47989 and mutual-TLS on 47984. Serves `/serverinfo`,
|
||||
//! the `/pair` flow, `/applist`, and `/launch`/`/resume`/`/cancel`, plus a lumen-only
|
||||
//! the `/pair` flow, `/applist`, and `/launch`/`/resume`/`/cancel`, plus a punktfunk-only
|
||||
//! `/pin` endpoint to deliver the Moonlight-displayed PIN. Over HTTPS the client is
|
||||
//! mutual-TLS-authenticated, so `/serverinfo` reports `PairStatus=1` there.
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ pub fn spawn(state: Arc<AppState>) -> Result<()> {
|
||||
.with_context(|| format!("bind RTSP {RTSP_PORT}"))?;
|
||||
tracing::info!(port = RTSP_PORT, "RTSP listening");
|
||||
std::thread::Builder::new()
|
||||
.name("lumen-rtsp".into())
|
||||
.name("punktfunk-rtsp".into())
|
||||
.spawn(move || {
|
||||
for conn in listener.incoming() {
|
||||
match conn {
|
||||
+12
-12
@@ -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 (`LUMEN_VIDEO_SOURCE=portal`, the M0 PipeWire path) or
|
||||
//! either real portal desktop capture (`PUNKTFUNK_VIDEO_SOURCE=portal`, the M0 PipeWire path) or
|
||||
//! a synthetic test pattern (default). Runs on its own native thread.
|
||||
|
||||
use super::video::{FrameType, VideoPacketizer};
|
||||
@@ -42,7 +42,7 @@ pub fn start(
|
||||
video_cap: CapturerSlot,
|
||||
) {
|
||||
let _ = std::thread::Builder::new()
|
||||
.name("lumen-video".into())
|
||||
.name("punktfunk-video".into())
|
||||
.spawn(move || {
|
||||
tracing::info!(?cfg, "video stream starting");
|
||||
if let Err(e) = run(cfg, app.as_ref(), &running, &force_idr, &video_cap) {
|
||||
@@ -83,7 +83,7 @@ fn run(
|
||||
// request and capture it (no scaling). Self-contained — deliberately NOT pooled in
|
||||
// `video_cap`, since a reconnect at a different resolution needs a freshly-sized output; the
|
||||
// output is released when this capturer drops at stream end (RAII via its keepalive).
|
||||
if std::env::var("LUMEN_VIDEO_SOURCE").as_deref() == Ok("virtual") {
|
||||
if std::env::var("PUNKTFUNK_VIDEO_SOURCE").as_deref() == Ok("virtual") {
|
||||
// The launched app picks the compositor (e.g. gamescope for game entries) and the
|
||||
// nested command; env vars remain manual overrides / fallbacks.
|
||||
let compositor = app
|
||||
@@ -93,7 +93,7 @@ fn run(
|
||||
if let Some(cmd) = app.and_then(|a| a.cmd.as_deref()) {
|
||||
// The gamescope backend reads the nested command from this env var; setting it
|
||||
// per-launch is safe (one stream session at a time).
|
||||
std::env::set_var("LUMEN_GAMESCOPE_APP", cmd);
|
||||
std::env::set_var("PUNKTFUNK_GAMESCOPE_APP", cmd);
|
||||
}
|
||||
tracing::info!(
|
||||
?compositor,
|
||||
@@ -104,7 +104,7 @@ fn run(
|
||||
);
|
||||
let mut vd = crate::vdisplay::open(compositor).context("open virtual display")?;
|
||||
let vout = vd
|
||||
.create(lumen_core::Mode {
|
||||
.create(punktfunk_core::Mode {
|
||||
width: cfg.width,
|
||||
height: cfg.height,
|
||||
refresh_hz: cfg.fps,
|
||||
@@ -123,7 +123,7 @@ fn run(
|
||||
tracing::info!("video source: reusing capturer");
|
||||
c
|
||||
}
|
||||
None if std::env::var("LUMEN_VIDEO_SOURCE").is_ok_and(|v| v == "portal") => {
|
||||
None if std::env::var("PUNKTFUNK_VIDEO_SOURCE").is_ok_and(|v| v == "portal") => {
|
||||
tracing::info!("video source: portal desktop capture");
|
||||
capture::open_portal_monitor().context("open portal capturer")?
|
||||
}
|
||||
@@ -202,7 +202,7 @@ fn spawn_sender(
|
||||
drop_pct: u32,
|
||||
) -> Result<()> {
|
||||
std::thread::Builder::new()
|
||||
.name("lumen-send".into())
|
||||
.name("punktfunk-send".into())
|
||||
.spawn(move || {
|
||||
// Chunk pacing: 16 packets per burst, bursts spread across the send budget.
|
||||
const PACE_CHUNK: usize = 16;
|
||||
@@ -276,8 +276,8 @@ fn stream_body(
|
||||
frame.is_cuda(),
|
||||
)
|
||||
.context("open NVENC for stream")?;
|
||||
// FEC overhead percent (Sunshine default 20). Override with LUMEN_FEC_PCT (0 = data-only).
|
||||
let fec_pct: u8 = std::env::var("LUMEN_FEC_PCT")
|
||||
// FEC overhead percent (Sunshine default 20). Override with PUNKTFUNK_FEC_PCT (0 = data-only).
|
||||
let fec_pct: u8 = std::env::var("PUNKTFUNK_FEC_PCT")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(20);
|
||||
@@ -294,7 +294,7 @@ fn stream_body(
|
||||
let mut fps_t = Instant::now();
|
||||
let stream_start = Instant::now();
|
||||
// Test knob: drop this % of outbound packets to exercise FEC recovery (0 = off).
|
||||
let drop_pct: u32 = std::env::var("LUMEN_VIDEO_DROP")
|
||||
let drop_pct: u32 = std::env::var("PUNKTFUNK_VIDEO_DROP")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
@@ -313,9 +313,9 @@ fn stream_body(
|
||||
drop_pct,
|
||||
)?;
|
||||
|
||||
// Per-stage timing (LUMEN_PERF=1): max µs/stage per second + unique vs re-encoded frames,
|
||||
// Per-stage timing (PUNKTFUNK_PERF=1): max µs/stage per second + unique vs re-encoded frames,
|
||||
// to pinpoint stalls. `unique` counts genuinely-new captured frames (vs re-encoded holds).
|
||||
let perf = std::env::var_os("LUMEN_PERF").is_some();
|
||||
let perf = std::env::var_os("PUNKTFUNK_PERF").is_some();
|
||||
let (mut mx_cap, mut mx_enc, mut mx_pkt, mut mx_send, mut mx_pkts, mut uniq) =
|
||||
(0u128, 0u128, 0u128, 0u128, 0usize, 0u32);
|
||||
// Absolute next-frame deadline — the single pacing clock for the loop.
|
||||
+2
-2
@@ -6,7 +6,7 @@
|
||||
//! `docs/research/gamestream-protocol-research.json` (video plane).
|
||||
//!
|
||||
//! FEC (P1.5): each block carries `m = ⌈k·pct/100⌉` Reed–Solomon parity shards generated by
|
||||
//! `lumen_core::fec::Gf8Coder` (the nanors-compatible Cauchy GF(2⁸) coder). Crucially, RS runs
|
||||
//! `punktfunk_core::fec::Gf8Coder` (the nanors-compatible Cauchy GF(2⁸) coder). Crucially, RS runs
|
||||
//! over the **whole `blocksize` shard** — Moonlight decodes over `packetSize + 16` bytes from
|
||||
//! the datagram start (`RtpVideoQueue.c`), and rejects a recovered shard whose reconstructed
|
||||
//! `flags` byte isn't valid — so the NV header fields RS must reproduce (streamPacketIndex,
|
||||
@@ -15,7 +15,7 @@
|
||||
//! Sunshine `stream.cpp`. `pct = 0` falls back to data-shards-only. Plaintext (AES-GCM video
|
||||
//! encryption is negotiated off for now).
|
||||
|
||||
use lumen_core::fec::{ErasureCoder, Gf8Coder};
|
||||
use punktfunk_core::fec::{ErasureCoder, Gf8Coder};
|
||||
|
||||
/// RTP `header` byte: version 2 (0x80) | extension (0x10) — Moonlight keys on the extension.
|
||||
const RTP_HEADER_BYTE: u8 = 0x80 | 0x10;
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Input injection (plan §4): turn client [`lumen_core::input::InputEvent`]s into host input.
|
||||
//! Input injection (plan §4): turn client [`punktfunk_core::input::InputEvent`]s into host input.
|
||||
//!
|
||||
//! The headless Sway compositor runs with `WLR_LIBINPUT_NO_DEVICES=1`, so kernel `uinput`
|
||||
//! devices are never picked up. Instead we inject through the wlroots virtual-input Wayland
|
||||
@@ -10,7 +10,7 @@
|
||||
//! keysyms correctly.
|
||||
|
||||
use anyhow::Result;
|
||||
use lumen_core::input::InputEvent;
|
||||
use punktfunk_core::input::InputEvent;
|
||||
|
||||
/// Injects input events into the host session. Not `Send`: an injector owns compositor
|
||||
/// resources (a Wayland connection, an xkb state) and lives entirely on the control thread
|
||||
@@ -77,9 +77,9 @@ pub fn open(backend: Backend) -> Result<Box<dyn InputInjector>> {
|
||||
/// portal), so a gamescope session injects directly into it. wlroots/Sway only implements the
|
||||
/// ScreenCast portal (no RemoteDesktop), so libei can't run there — use the wlr virtual-input
|
||||
/// protocols. KWin and GNOME implement RemoteDesktop but not the wlr protocols, so use libei.
|
||||
/// `LUMEN_INPUT_BACKEND=wlr|libei|gamescope|uinput` overrides the auto-detection.
|
||||
/// `PUNKTFUNK_INPUT_BACKEND=wlr|libei|gamescope|uinput` overrides the auto-detection.
|
||||
pub fn default_backend() -> Backend {
|
||||
if let Ok(v) = std::env::var("LUMEN_INPUT_BACKEND") {
|
||||
if let Ok(v) = std::env::var("PUNKTFUNK_INPUT_BACKEND") {
|
||||
match v.trim().to_ascii_lowercase().as_str() {
|
||||
"wlr" | "wlroots" | "wlrvirtual" => return Backend::WlrVirtual,
|
||||
"libei" | "ei" | "portal" => return Backend::Libei,
|
||||
@@ -87,11 +87,13 @@ pub fn default_backend() -> Backend {
|
||||
"uinput" => return Backend::Uinput,
|
||||
other => tracing::warn!(
|
||||
value = other,
|
||||
"unknown LUMEN_INPUT_BACKEND — auto-detecting"
|
||||
"unknown PUNKTFUNK_INPUT_BACKEND — auto-detecting"
|
||||
),
|
||||
}
|
||||
}
|
||||
if std::env::var("LUMEN_COMPOSITOR").is_ok_and(|v| v.trim().eq_ignore_ascii_case("gamescope")) {
|
||||
if std::env::var("PUNKTFUNK_COMPOSITOR")
|
||||
.is_ok_and(|v| v.trim().eq_ignore_ascii_case("gamescope"))
|
||||
{
|
||||
return Backend::GamescopeEi;
|
||||
}
|
||||
let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
|
||||
+2
-2
@@ -13,7 +13,7 @@
|
||||
//!
|
||||
//! All ioctl numbers/struct layouts below were verified against this generation's
|
||||
//! `<linux/uinput.h>` on x86_64. `/dev/uinput` needs a udev rule + `input` group membership
|
||||
//! (see `scripts/60-lumen.rules`); creation fails with a clear error otherwise.
|
||||
//! (see `scripts/60-punktfunk.rules`); creation fails with a clear error otherwise.
|
||||
|
||||
use crate::gamestream::gamepad::{self, GamepadFrame, MAX_PADS};
|
||||
use anyhow::{bail, Result};
|
||||
@@ -213,7 +213,7 @@ impl VirtualPad {
|
||||
if raw < 0 {
|
||||
bail!(
|
||||
"open /dev/uinput: {} (install the udev rule granting the 'input' group access \
|
||||
— see scripts/60-lumen.rules — and add the user to the 'input' group)",
|
||||
— see scripts/60-punktfunk.rules — and add the user to the 'input' group)",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
@@ -28,7 +28,7 @@ use ashpd::desktop::{
|
||||
CreateSessionOptions, PersistMode,
|
||||
};
|
||||
use futures_util::StreamExt;
|
||||
use lumen_core::input::{InputEvent, InputKind};
|
||||
use punktfunk_core::input::{InputEvent, InputKind};
|
||||
use reis::ei;
|
||||
use reis::event::{DeviceCapability, EiEvent};
|
||||
use std::os::unix::net::UnixStream;
|
||||
@@ -61,7 +61,7 @@ impl LibeiInjector {
|
||||
pub fn open_with(source: EiSource) -> Result<Self> {
|
||||
let (tx, rx) = unbounded_channel::<InputEvent>();
|
||||
std::thread::Builder::new()
|
||||
.name("lumen-libei".into())
|
||||
.name("punktfunk-libei".into())
|
||||
.spawn(move || worker(rx, source))
|
||||
.map_err(|e| anyhow!("spawn libei worker thread: {e}"))?;
|
||||
// Return immediately — the portal/socket handshake must NOT run on the caller's
|
||||
@@ -156,7 +156,7 @@ async fn connect(source: EiSource) -> Result<Connected> {
|
||||
};
|
||||
let context = ei::Context::new(stream).map_err(|e| anyhow!("reis EI context: {e}"))?;
|
||||
let (_conn, events) = context
|
||||
.handshake_tokio("lumen-host", ei::handshake::ContextType::Sender)
|
||||
.handshake_tokio("punktfunk-host", ei::handshake::ContextType::Sender)
|
||||
.await
|
||||
.map_err(|e| anyhow!("EI handshake: {e}"))?;
|
||||
Ok((portal, context, events))
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
use super::{gs_button_to_evdev, vk_to_evdev, InputEvent, InputInjector};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use lumen_core::input::InputKind;
|
||||
use punktfunk_core::input::InputKind;
|
||||
use std::io::Write;
|
||||
use std::os::fd::{AsFd, FromRawFd};
|
||||
use std::time::Instant;
|
||||
@@ -261,7 +261,7 @@ impl InputInjector for WlrootsInjector {
|
||||
|
||||
/// Create an anonymous in-memory file holding `s` + a trailing NUL (for the keymap fd).
|
||||
fn memfd_with(s: &str) -> Result<std::fs::File> {
|
||||
let name = b"lumen-keymap\0";
|
||||
let name = b"punktfunk-keymap\0";
|
||||
let fd = unsafe { libc::memfd_create(name.as_ptr() as *const libc::c_char, libc::MFD_CLOEXEC) };
|
||||
if fd < 0 {
|
||||
bail!("memfd_create failed: {}", std::io::Error::last_os_error());
|
||||
@@ -1,5 +1,5 @@
|
||||
//! M0 — the pipeline spike (plan §8): capture → NVENC encode → playable file, with the
|
||||
//! encoded access units also fed through a `lumen_core` host→client `Session` over an
|
||||
//! 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.
|
||||
//!
|
||||
@@ -11,8 +11,8 @@
|
||||
use crate::capture::{self, Capturer, SyntheticCapturer};
|
||||
use crate::encode::{self, Codec, EncodedFrame, Encoder};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use lumen_core::packet::{FLAG_PIC, FLAG_SOF};
|
||||
use lumen_core::{Config, Role, Session};
|
||||
use punktfunk_core::packet::{FLAG_PIC, FLAG_SOF};
|
||||
use punktfunk_core::{Config, Role, Session};
|
||||
use std::fs::File;
|
||||
use std::io::{BufWriter, Write};
|
||||
use std::path::PathBuf;
|
||||
@@ -41,7 +41,7 @@ pub struct Options {
|
||||
pub bitrate_bps: u64,
|
||||
/// Raw Annex-B elementary-stream sink (`.h265`/`.h264`/`.ivf-less .obu`); playable.
|
||||
pub out: PathBuf,
|
||||
/// Also round-trip every AU through a `lumen_core` host→client loopback and verify.
|
||||
/// Also round-trip every AU through a `punktfunk_core` host→client loopback and verify.
|
||||
pub loopback: bool,
|
||||
}
|
||||
|
||||
@@ -66,11 +66,11 @@ pub fn run(opts: Options) -> Result<()> {
|
||||
width = opts.width,
|
||||
height = opts.height,
|
||||
?compositor,
|
||||
"M0 source: virtual output (LUMEN_COMPOSITOR)"
|
||||
"M0 source: virtual output (PUNKTFUNK_COMPOSITOR)"
|
||||
);
|
||||
let mut vd = crate::vdisplay::open(compositor).context("open virtual display")?;
|
||||
let vout = vd
|
||||
.create(lumen_core::Mode {
|
||||
.create(punktfunk_core::Mode {
|
||||
width: opts.width,
|
||||
height: opts.height,
|
||||
refresh_hz: opts.fps,
|
||||
@@ -112,7 +112,7 @@ pub fn run(opts: Options) -> Result<()> {
|
||||
);
|
||||
|
||||
let mut lb = if opts.loopback {
|
||||
Some(Loopback::new().context("build lumen-core loopback")?)
|
||||
Some(Loopback::new().context("build punktfunk-core loopback")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -153,7 +153,7 @@ pub fn run(opts: Options) -> Result<()> {
|
||||
lb.report();
|
||||
if lb.mismatches > 0 || lb.recovered != lb.submitted {
|
||||
return Err(anyhow!(
|
||||
"lumen-core loopback verification FAILED: {} mismatches, {}/{} AUs recovered",
|
||||
"punktfunk-core loopback verification FAILED: {} mismatches, {}/{} AUs recovered",
|
||||
lb.mismatches,
|
||||
lb.recovered,
|
||||
lb.submitted
|
||||
@@ -191,7 +191,7 @@ fn drain_encoder(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A host↔client `lumen_core` pair over a lossless in-process loopback. Each encoded AU is
|
||||
/// 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).
|
||||
struct Loopback {
|
||||
@@ -205,7 +205,7 @@ struct Loopback {
|
||||
|
||||
impl Loopback {
|
||||
fn new() -> Result<Loopback> {
|
||||
let (host_tx, client_tx) = lumen_core::transport::loopback_pair(0, 0);
|
||||
let (host_tx, client_tx) = punktfunk_core::transport::loopback_pair(0, 0);
|
||||
let host = Session::new(Config::p1_defaults(Role::Host), Box::new(host_tx))
|
||||
.map_err(|e| anyhow!("host session: {e:?}"))?;
|
||||
let client = Session::new(Config::p1_defaults(Role::Client), Box::new(client_tx))
|
||||
@@ -246,7 +246,7 @@ impl Loopback {
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(lumen_core::LumenError::NoFrame) => break,
|
||||
Err(punktfunk_core::PunktfunkError::NoFrame) => break,
|
||||
Err(e) => return Err(anyhow!("client poll_frame: {e:?}")),
|
||||
}
|
||||
}
|
||||
@@ -259,7 +259,7 @@ impl Loopback {
|
||||
recovered = self.recovered,
|
||||
mismatches = self.mismatches,
|
||||
bytes = self.bytes,
|
||||
"lumen-core loopback: AUs FEC-packetized → sent → reassembled & verified"
|
||||
"punktfunk-core loopback: AUs FEC-packetized → sent → reassembled & verified"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
//! M3 — the `lumen/1` native host: QUIC control plane + the hardened M1 data plane over UDP.
|
||||
//! This is lumen's own protocol, past the GameStream compatibility layer:
|
||||
//! M3 — the `punktfunk/1` native host: QUIC control plane + the hardened M1 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;
|
||||
//! * the client's Hello requests a display mode and the host creates a **native virtual
|
||||
@@ -9,26 +9,26 @@
|
||||
//! * video frames carry a wall-clock `pts_ns`, so a same-host client measures the full
|
||||
//! capture→encode→FEC→UDP→reassemble latency per frame.
|
||||
//!
|
||||
//! `lumen-host m3-host [--port 9777] [--source synthetic|virtual] [--seconds 30]
|
||||
//! `punktfunk-host m3-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); `lumen-client-rs --connect host:9777` is the counterpart.
|
||||
//! encoder are single-tenant); `punktfunk-client-rs --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 →
|
||||
//! client QUIC datagrams tagged [`lumen_core::quic::AUDIO_MAGIC`]) and **gamepads** (client
|
||||
//! client QUIC datagrams tagged [`punktfunk_core::quic::AUDIO_MAGIC`]) and **gamepads** (client
|
||||
//! GamepadButton/GamepadAxis datagrams accumulated into per-pad state for the virtual xpad;
|
||||
//! force feedback flows back as [`lumen_core::quic::RUMBLE_MAGIC`] datagrams).
|
||||
//! force feedback flows back as [`punktfunk_core::quic::RUMBLE_MAGIC`] datagrams).
|
||||
//!
|
||||
//! Trust: the host serves with its persistent identity (`~/.config/lumen/cert.pem`, shared
|
||||
//! Trust: the host serves with its persistent identity (`~/.config/punktfunk/cert.pem`, shared
|
||||
//! with GameStream pairing) and logs the SHA-256 fingerprint clients pin.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use lumen_core::config::{FecConfig, FecScheme, Role};
|
||||
use lumen_core::input::{InputEvent, InputKind};
|
||||
use lumen_core::packet::{FLAG_PIC, FLAG_SOF};
|
||||
use lumen_core::quic::{endpoint, io, Hello, Start, Welcome};
|
||||
use lumen_core::transport::UdpTransport;
|
||||
use lumen_core::Session;
|
||||
use punktfunk_core::config::{FecConfig, FecScheme, Role};
|
||||
use punktfunk_core::input::{InputEvent, InputKind};
|
||||
use punktfunk_core::packet::{FLAG_PIC, FLAG_SOF};
|
||||
use punktfunk_core::quic::{endpoint, io, Hello, Start, Welcome};
|
||||
use punktfunk_core::transport::UdpTransport;
|
||||
use punktfunk_core::Session;
|
||||
use rand::RngCore;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
@@ -88,7 +88,7 @@ fn fingerprint_hex(fp: &[u8; 32]) -> String {
|
||||
/// keeps serving — only endpoint-level failures are fatal.
|
||||
async fn serve(opts: M3Options) -> Result<()> {
|
||||
let identity = crate::gamestream::cert::ServerIdentity::load_or_create()
|
||||
.context("load host identity (~/.config/lumen)")?;
|
||||
.context("load host identity (~/.config/punktfunk)")?;
|
||||
let fingerprint = endpoint::fingerprint_of_pem(&identity.cert_pem)
|
||||
.map_err(|e| anyhow!("cert fingerprint: {e}"))?;
|
||||
let ep = endpoint::server_with_identity(
|
||||
@@ -101,7 +101,7 @@ async fn serve(opts: M3Options) -> Result<()> {
|
||||
port = opts.port,
|
||||
source = ?opts.source,
|
||||
fingerprint = %fingerprint_hex(&fingerprint),
|
||||
"lumen/1 host listening (QUIC) — clients pin this fingerprint"
|
||||
"punktfunk/1 host listening (QUIC) — clients pin this fingerprint"
|
||||
);
|
||||
|
||||
// One audio capturer for the whole host lifetime, handed from session to session
|
||||
@@ -122,7 +122,7 @@ async fn serve(opts: M3Options) -> Result<()> {
|
||||
}
|
||||
};
|
||||
let peer = conn.remote_address();
|
||||
tracing::info!(%peer, "lumen/1 client connected");
|
||||
tracing::info!(%peer, "punktfunk/1 client connected");
|
||||
if let Err(e) = serve_session(conn, &opts, &audio_cap).await {
|
||||
tracing::warn!(%peer, error = %format!("{e:#}"), "session ended with error");
|
||||
} else {
|
||||
@@ -164,10 +164,10 @@ async fn serve_session(
|
||||
let hello = Hello::decode(&io::read_msg(&mut recv).await?)
|
||||
.map_err(|e| anyhow!("Hello decode: {e:?}"))?;
|
||||
anyhow::ensure!(
|
||||
hello.abi_version == lumen_core::ABI_VERSION,
|
||||
hello.abi_version == punktfunk_core::ABI_VERSION,
|
||||
"ABI mismatch: client {} host {}",
|
||||
hello.abi_version,
|
||||
lumen_core::ABI_VERSION
|
||||
punktfunk_core::ABI_VERSION
|
||||
);
|
||||
crate::encode::validate_dimensions(
|
||||
crate::encode::Codec::H265,
|
||||
@@ -184,10 +184,10 @@ async fn serve_session(
|
||||
let mut key = [0u8; 16];
|
||||
rand::thread_rng().fill_bytes(&mut key);
|
||||
let welcome = Welcome {
|
||||
abi_version: lumen_core::ABI_VERSION,
|
||||
abi_version: punktfunk_core::ABI_VERSION,
|
||||
udp_port,
|
||||
mode: hello.mode,
|
||||
// The post-GameStream point of lumen/1: Leopard GF(2¹⁶) FEC + real encryption.
|
||||
// The post-GameStream point of punktfunk/1: Leopard GF(2¹⁶) FEC + real encryption.
|
||||
fec: FecConfig {
|
||||
scheme: FecScheme::Gf16,
|
||||
fec_percent: 20,
|
||||
@@ -196,7 +196,7 @@ async fn serve_session(
|
||||
shard_payload: 1200,
|
||||
encrypt: true,
|
||||
key,
|
||||
salt: *b"lmn1",
|
||||
salt: *b"pkf1",
|
||||
frames: match source {
|
||||
M3Source::Synthetic => frames,
|
||||
M3Source::Virtual => 0, // unbounded — client streams until we close
|
||||
@@ -222,7 +222,7 @@ async fn serve_session(
|
||||
let input_handle = {
|
||||
let conn = conn.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("lumen-m3-input".into())
|
||||
.name("punktfunk-m3-input".into())
|
||||
.spawn(move || input_thread(input_rx, conn))
|
||||
.context("spawn input thread")?
|
||||
};
|
||||
@@ -260,7 +260,7 @@ async fn serve_session(
|
||||
let stop = stop.clone();
|
||||
let cap = audio_cap.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("lumen-m3-audio".into())
|
||||
.name("punktfunk-m3-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()
|
||||
@@ -313,8 +313,8 @@ async fn serve_session(
|
||||
result
|
||||
}
|
||||
|
||||
/// Per-pad accumulated state: lumen/1 gamepad events are incremental (one button or axis
|
||||
/// per datagram, see `lumen_core::input::gamepad`), the virtual xpad applies full frames.
|
||||
/// Per-pad accumulated state: punktfunk/1 gamepad events are incremental (one button or axis
|
||||
/// per datagram, see `punktfunk_core::input::gamepad`), the virtual xpad applies full frames.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct PadState {
|
||||
buttons: u32,
|
||||
@@ -337,7 +337,7 @@ impl PadState {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
use lumen_core::input::gamepad::*;
|
||||
use punktfunk_core::input::gamepad::*;
|
||||
let stick = ev.x.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
|
||||
let trigger = ev.x.clamp(0, 255) as u8;
|
||||
match ev.code {
|
||||
@@ -403,7 +403,7 @@ fn input_thread(rx: std::sync::mpsc::Receiver<InputEvent>, conn: quinn::Connecti
|
||||
let backend = crate::inject::default_backend();
|
||||
match crate::inject::open(backend) {
|
||||
Ok(i) => {
|
||||
tracing::info!(?backend, "lumen/1 input injector opened");
|
||||
tracing::info!(?backend, "punktfunk/1 input injector opened");
|
||||
injector = Some(i);
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -430,14 +430,14 @@ fn input_thread(rx: std::sync::mpsc::Receiver<InputEvent>, conn: quinn::Connecti
|
||||
*s = (low, high);
|
||||
rumble_seen[pad as usize] = true;
|
||||
}
|
||||
let d = lumen_core::quic::encode_rumble_datagram(pad, low, high);
|
||||
let d = punktfunk_core::quic::encode_rumble_datagram(pad, low, high);
|
||||
let _ = conn.send_datagram(d.to_vec().into());
|
||||
});
|
||||
if last_refresh.elapsed() >= std::time::Duration::from_millis(500) {
|
||||
last_refresh = std::time::Instant::now();
|
||||
for (i, &(low, high)) in rumble_state.iter().enumerate() {
|
||||
if rumble_seen[i] {
|
||||
let d = lumen_core::quic::encode_rumble_datagram(i as u16, low, high);
|
||||
let d = punktfunk_core::quic::encode_rumble_datagram(i as u16, low, high);
|
||||
let _ = conn.send_datagram(d.to_vec().into());
|
||||
}
|
||||
}
|
||||
@@ -462,7 +462,7 @@ fn audio_thread(conn: quinn::Connection, stop: Arc<AtomicBool>, audio_cap: Audio
|
||||
None => match crate::audio::open_audio_capture() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %format!("{e:#}"), "lumen/1 audio unavailable — session continues without it");
|
||||
tracing::warn!(error = %format!("{e:#}"), "punktfunk/1 audio unavailable — session continues without it");
|
||||
return;
|
||||
}
|
||||
},
|
||||
@@ -487,7 +487,7 @@ fn audio_thread(conn: quinn::Connection, stop: Arc<AtomicBool>, audio_cap: Audio
|
||||
let mut opus_buf = vec![0u8; 1500];
|
||||
let mut seq: u32 = 0;
|
||||
let mut capture_dead = false;
|
||||
tracing::info!("lumen/1 audio streaming (Opus 48 kHz stereo, 5 ms datagrams)");
|
||||
tracing::info!("punktfunk/1 audio streaming (Opus 48 kHz stereo, 5 ms datagrams)");
|
||||
'session: while !stop.load(Ordering::SeqCst) {
|
||||
let chunk = match capturer.next_chunk() {
|
||||
Ok(c) => c,
|
||||
@@ -503,7 +503,8 @@ fn audio_thread(conn: quinn::Connection, stop: Arc<AtomicBool>, audio_cap: Audio
|
||||
let pts_ns = now_ns();
|
||||
match enc.encode_float(&frame, &mut opus_buf) {
|
||||
Ok(n) => {
|
||||
let d = lumen_core::quic::encode_audio_datagram(seq, pts_ns, &opus_buf[..n]);
|
||||
let d =
|
||||
punktfunk_core::quic::encode_audio_datagram(seq, pts_ns, &opus_buf[..n]);
|
||||
if conn.send_datagram(d.into()).is_err() {
|
||||
break 'session; // connection gone
|
||||
}
|
||||
@@ -520,12 +521,12 @@ fn audio_thread(conn: quinn::Connection, stop: Arc<AtomicBool>, audio_cap: Audio
|
||||
}
|
||||
}
|
||||
|
||||
/// Stub — lumen/1 audio needs Linux (PipeWire capture + libopus); non-Linux dev builds
|
||||
/// Stub — punktfunk/1 audio needs Linux (PipeWire capture + libopus); non-Linux dev builds
|
||||
/// run sessions without it, same as when the capturer fails to open.
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn audio_thread(_conn: quinn::Connection, _stop: Arc<AtomicBool>, _audio_cap: AudioCapSlot) {
|
||||
tracing::warn!(
|
||||
"lumen/1 audio requires Linux (PipeWire + libopus) — session continues without it"
|
||||
"punktfunk/1 audio requires Linux (PipeWire + libopus) — session continues without it"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -545,16 +546,16 @@ fn synthetic_stream(session: &mut Session, frames: u32, stop: &AtomicBool) -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Real capture→encode→lumen/1: a native virtual output at the client's mode, NVENC AUs
|
||||
/// Real capture→encode→punktfunk/1: a native virtual output at the client's mode, NVENC AUs
|
||||
/// stamped with the capture wall clock (the client derives per-frame pipeline latency).
|
||||
fn virtual_stream(
|
||||
session: &mut Session,
|
||||
mode: lumen_core::Mode,
|
||||
mode: punktfunk_core::Mode,
|
||||
seconds: u32,
|
||||
stop: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let compositor = crate::vdisplay::detect().context("detect compositor")?;
|
||||
tracing::info!(?compositor, ?mode, "lumen/1 virtual display");
|
||||
tracing::info!(?compositor, ?mode, "punktfunk/1 virtual display");
|
||||
let mut vd = crate::vdisplay::open(compositor)?;
|
||||
let vout = vd.create(mode).context("create virtual output")?;
|
||||
let mut capturer =
|
||||
@@ -600,7 +601,7 @@ fn virtual_stream(
|
||||
None => next = std::time::Instant::now(),
|
||||
}
|
||||
}
|
||||
tracing::info!(sent, "lumen/1 virtual stream complete");
|
||||
tracing::info!(sent, "punktfunk/1 virtual stream complete");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -622,7 +623,7 @@ mod tests {
|
||||
/// Incremental wire events accumulate into the full pad frame the virtual xpad applies.
|
||||
#[test]
|
||||
fn gamepad_accumulator() {
|
||||
use lumen_core::input::gamepad::*;
|
||||
use punktfunk_core::input::gamepad::*;
|
||||
let mut s = PadState::default();
|
||||
assert!(s.apply(&gp(InputKind::GamepadButton, BTN_A, 1, 0)));
|
||||
assert!(s.apply(&gp(InputKind::GamepadButton, BTN_LB, 1, 0)));
|
||||
@@ -640,20 +641,22 @@ mod tests {
|
||||
assert_eq!(s.left_trigger, 255);
|
||||
assert!(!s.apply(&gp(InputKind::GamepadAxis, 42, 1, 0)));
|
||||
|
||||
// The lumen/1 button bits are the GameStream bits — one wire contract end to end.
|
||||
// The punktfunk/1 button bits are the GameStream bits — one wire contract end to end.
|
||||
assert_eq!(BTN_A, crate::gamestream::gamepad::BTN_A);
|
||||
assert_eq!(BTN_GUIDE, crate::gamestream::gamepad::BTN_GUIDE);
|
||||
assert_eq!(BTN_DPAD_UP, crate::gamestream::gamepad::BTN_DPAD_UP);
|
||||
}
|
||||
|
||||
/// Pull and byte-verify `count` synthetic frames through the C ABI connection.
|
||||
unsafe fn pull_verified(conn: *mut lumen_core::abi::LumenConnection, count: u32) {
|
||||
use lumen_core::error::LumenStatus;
|
||||
unsafe fn pull_verified(conn: *mut punktfunk_core::abi::PunktfunkConnection, count: u32) {
|
||||
use punktfunk_core::error::PunktfunkStatus;
|
||||
let mut got = 0u32;
|
||||
let mut frame = unsafe { std::mem::zeroed() };
|
||||
while got < count {
|
||||
match unsafe { lumen_core::abi::lumen_connection_next_au(conn, &mut frame, 2000) } {
|
||||
LumenStatus::Ok => {
|
||||
match unsafe {
|
||||
punktfunk_core::abi::punktfunk_connection_next_au(conn, &mut frame, 2000)
|
||||
} {
|
||||
PunktfunkStatus::Ok => {
|
||||
let data = unsafe { std::slice::from_raw_parts(frame.data, frame.len) };
|
||||
let idx = u32::from_le_bytes(data[0..4].try_into().unwrap());
|
||||
assert_eq!(
|
||||
@@ -663,24 +666,24 @@ mod tests {
|
||||
);
|
||||
got += 1;
|
||||
}
|
||||
LumenStatus::NoFrame => continue,
|
||||
PunktfunkStatus::NoFrame => continue,
|
||||
other => panic!("next_au: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// End-to-end through the C ABI — the exact contract platform clients (Swift) link:
|
||||
/// in-process lumen/1 host, `lumen_connect` (TOFU → pinned reconnect) →
|
||||
/// `lumen_connection_next_au` pulls verified frames → `lumen_connection_send_input`
|
||||
/// enqueues → `lumen_connection_close`. Three sequential sessions against ONE host
|
||||
/// in-process punktfunk/1 host, `punktfunk_connect` (TOFU → pinned reconnect) →
|
||||
/// `punktfunk_connection_next_au` pulls verified frames → `punktfunk_connection_send_input`
|
||||
/// enqueues → `punktfunk_connection_close`. Three sequential sessions against ONE host
|
||||
/// process prove the persistent listener, and a wrong pin is rejected.
|
||||
#[test]
|
||||
fn c_abi_connection_roundtrip() {
|
||||
use lumen_core::abi::{
|
||||
lumen_connect, lumen_connection_close, lumen_connection_mode,
|
||||
lumen_connection_send_input,
|
||||
use punktfunk_core::abi::{
|
||||
punktfunk_connect, punktfunk_connection_close, punktfunk_connection_mode,
|
||||
punktfunk_connection_send_input,
|
||||
};
|
||||
use lumen_core::error::LumenStatus;
|
||||
use punktfunk_core::error::PunktfunkStatus;
|
||||
|
||||
let host = std::thread::spawn(|| {
|
||||
run(M3Options {
|
||||
@@ -697,7 +700,7 @@ mod tests {
|
||||
let addr = std::ffi::CString::new("127.0.0.1").unwrap();
|
||||
let mut observed = [0u8; 32];
|
||||
let conn = unsafe {
|
||||
lumen_connect(
|
||||
punktfunk_connect(
|
||||
addr.as_ptr(),
|
||||
19777,
|
||||
1280,
|
||||
@@ -708,20 +711,20 @@ mod tests {
|
||||
10_000,
|
||||
)
|
||||
};
|
||||
assert!(!conn.is_null(), "lumen_connect failed");
|
||||
assert!(!conn.is_null(), "punktfunk_connect failed");
|
||||
assert_ne!(observed, [0u8; 32], "fingerprint not reported");
|
||||
|
||||
let (mut w, mut h, mut hz) = (0u32, 0u32, 0u32);
|
||||
assert_eq!(
|
||||
unsafe { lumen_connection_mode(conn, &mut w, &mut h, &mut hz) },
|
||||
LumenStatus::Ok
|
||||
unsafe { punktfunk_connection_mode(conn, &mut w, &mut h, &mut hz) },
|
||||
PunktfunkStatus::Ok
|
||||
);
|
||||
assert_eq!((w, h, hz), (1280, 720, 60));
|
||||
|
||||
unsafe { pull_verified(conn, 25) };
|
||||
|
||||
let ev = lumen_core::input::InputEvent {
|
||||
kind: lumen_core::input::InputKind::MouseMove,
|
||||
let ev = punktfunk_core::input::InputEvent {
|
||||
kind: punktfunk_core::input::InputKind::MouseMove,
|
||||
_pad: [0; 3],
|
||||
code: 0,
|
||||
x: 1,
|
||||
@@ -729,14 +732,14 @@ mod tests {
|
||||
flags: 0,
|
||||
};
|
||||
assert_eq!(
|
||||
unsafe { lumen_connection_send_input(conn, &ev) },
|
||||
LumenStatus::Ok
|
||||
unsafe { punktfunk_connection_send_input(conn, &ev) },
|
||||
PunktfunkStatus::Ok
|
||||
);
|
||||
unsafe { lumen_connection_close(conn) };
|
||||
unsafe { punktfunk_connection_close(conn) };
|
||||
|
||||
// Session 2 (same host process — the listener survived): pin the fingerprint.
|
||||
let conn2 = unsafe {
|
||||
lumen_connect(
|
||||
punktfunk_connect(
|
||||
addr.as_ptr(),
|
||||
19777,
|
||||
1280,
|
||||
@@ -749,12 +752,12 @@ mod tests {
|
||||
};
|
||||
assert!(!conn2.is_null(), "pinned reconnect failed");
|
||||
unsafe { pull_verified(conn2, 25) };
|
||||
unsafe { lumen_connection_close(conn2) };
|
||||
unsafe { punktfunk_connection_close(conn2) };
|
||||
|
||||
// Session 3: a wrong pin must be rejected by the handshake.
|
||||
let bad = [0xAAu8; 32];
|
||||
let conn3 = unsafe {
|
||||
lumen_connect(
|
||||
punktfunk_connect(
|
||||
addr.as_ptr(),
|
||||
19777,
|
||||
1280,
|
||||
@@ -771,7 +774,7 @@ mod tests {
|
||||
// handshake never yields a connection, so accept() is still waiting. Connect once
|
||||
// more (TOFU) to complete the host's third session and let it exit.
|
||||
let conn4 = unsafe {
|
||||
lumen_connect(
|
||||
punktfunk_connect(
|
||||
addr.as_ptr(),
|
||||
19777,
|
||||
1280,
|
||||
@@ -784,7 +787,7 @@ mod tests {
|
||||
};
|
||||
assert!(!conn4.is_null());
|
||||
unsafe { pull_verified(conn4, 25) };
|
||||
unsafe { lumen_connection_close(conn4) };
|
||||
unsafe { punktfunk_connection_close(conn4) };
|
||||
|
||||
host.join().unwrap().unwrap();
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
//! `lumen-host` — the Linux streaming host (plan §2, §6, §7).
|
||||
//! `punktfunk-host` — the Linux streaming host (plan §2, §6, §7).
|
||||
//!
|
||||
//! Creates a client-sized virtual display, captures it via PipeWire, encodes with
|
||||
//! VAAPI/NVENC, and hands encoded access units to `lumen_core` for FEC + packetization +
|
||||
//! VAAPI/NVENC, and hands encoded access units to `punktfunk_core` for FEC + packetization +
|
||||
//! pacing + send. Input flows back via libei/uinput. The platform backends are
|
||||
//! `#[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 `lumen_core` loopback. M2 wires the full P1 host that a stock
|
||||
//! the encoded AUs through a `punktfunk_core` loopback. M2 wires the full P1 host that a stock
|
||||
//! Moonlight client connects to.
|
||||
|
||||
// Scaffold: trait methods and config paths are defined ahead of their backends.
|
||||
@@ -33,7 +33,7 @@ use m0::{Options, Source};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
// Logs go to stderr so stdout stays machine-readable (`lumen-host openapi > spec.json`).
|
||||
// Logs go to stderr so stdout stays machine-readable (`punktfunk-host openapi > spec.json`).
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
|
||||
@@ -48,7 +48,10 @@ fn main() {
|
||||
}
|
||||
|
||||
fn real_main() -> Result<()> {
|
||||
tracing::info!("lumen-host (lumen_core ABI v{})", lumen_core::ABI_VERSION);
|
||||
tracing::info!(
|
||||
"punktfunk-host (punktfunk_core ABI v{})",
|
||||
punktfunk_core::ABI_VERSION
|
||||
);
|
||||
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
match args.first().map(String::as_str) {
|
||||
@@ -67,7 +70,7 @@ fn real_main() -> Result<()> {
|
||||
Some("zerocopy-probe") => zerocopy::probe(),
|
||||
// M0 pipeline spike.
|
||||
Some("m0") => m0::run(parse_m0(&args[1..])?),
|
||||
// M3: native lumen/1 host (QUIC control plane + UDP data plane).
|
||||
// M3: native punktfunk/1 host (QUIC control plane + UDP data plane).
|
||||
Some("m3-host") => {
|
||||
let get = |flag: &str| {
|
||||
args.iter()
|
||||
@@ -102,7 +105,7 @@ fn real_main() -> Result<()> {
|
||||
/// KWin/GNOME, wlr on Sway). Lets us validate input injection without a Moonlight client.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn input_test() -> Result<()> {
|
||||
use lumen_core::input::{InputEvent, InputKind};
|
||||
use punktfunk_core::input::{InputEvent, InputKind};
|
||||
use std::time::Duration;
|
||||
|
||||
let backend = inject::default_backend();
|
||||
@@ -188,7 +191,7 @@ fn parse_serve(args: &[String]) -> Result<mgmt::Options> {
|
||||
}
|
||||
// Flag wins over the environment so a unit file can set a default and a shell override it.
|
||||
if opts.token.is_none() {
|
||||
opts.token = std::env::var("LUMEN_MGMT_TOKEN")
|
||||
opts.token = std::env::var("PUNKTFUNK_MGMT_TOKEN")
|
||||
.ok()
|
||||
.filter(|t| !t.is_empty());
|
||||
}
|
||||
@@ -276,7 +279,7 @@ fn parse_m0(args: &[String]) -> Result<Options> {
|
||||
Codec::H265 => "h265",
|
||||
Codec::Av1 => "obu",
|
||||
};
|
||||
PathBuf::from(format!("/tmp/lumen-m0.{ext}"))
|
||||
PathBuf::from(format!("/tmp/punktfunk-m0.{ext}"))
|
||||
});
|
||||
|
||||
Ok(Options {
|
||||
@@ -294,18 +297,18 @@ fn parse_m0(args: &[String]) -> Result<Options> {
|
||||
|
||||
fn print_usage() {
|
||||
eprintln!(
|
||||
"lumen-host — Linux streaming host
|
||||
"punktfunk-host — Linux streaming host
|
||||
|
||||
USAGE:
|
||||
lumen-host serve [OPTIONS] GameStream host control plane (M2: mDNS + serverinfo …)
|
||||
punktfunk-host serve [OPTIONS] GameStream host control plane (M2: mDNS + serverinfo …)
|
||||
+ the management REST API
|
||||
lumen-host openapi print the management API's OpenAPI document (codegen)
|
||||
lumen-host m3-host [OPTIONS] native lumen/1 host (QUIC control plane + UDP data plane)
|
||||
lumen-host m0 [OPTIONS] M0 capture→encode→file pipeline spike
|
||||
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 m0 [OPTIONS] M0 capture→encode→file pipeline spike
|
||||
|
||||
SERVE OPTIONS:
|
||||
--mgmt-bind <IP:PORT> management API address (default: 127.0.0.1:47990)
|
||||
--mgmt-token <TOKEN> bearer token for the management API (or LUMEN_MGMT_TOKEN);
|
||||
--mgmt-token <TOKEN> bearer token for the management API (or PUNKTFUNK_MGMT_TOKEN);
|
||||
required when --mgmt-bind is not loopback
|
||||
|
||||
M3-HOST OPTIONS:
|
||||
@@ -324,14 +327,14 @@ 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/lumen-m0.<ext>)
|
||||
--no-loopback skip the lumen_core round-trip verification
|
||||
--out <PATH> raw Annex-B output (default: /tmp/punktfunk-m0.<ext>)
|
||||
--no-loopback skip the punktfunk_core round-trip verification
|
||||
-h, --help this help
|
||||
|
||||
NOTES:
|
||||
'portal' needs headless Sway + xdg-desktop-portal-wlr running in this session
|
||||
(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
|
||||
lumen_core host→client loopback that reassembles and byte-verifies each one."
|
||||
punktfunk_core host→client loopback that reassembles and byte-verifies each one."
|
||||
);
|
||||
}
|
||||
@@ -4,12 +4,12 @@
|
||||
//! the per-frame pipeline never touches this module.
|
||||
//!
|
||||
//! The API is versioned under `/api/v1` and described by an OpenAPI 3.1 document generated
|
||||
//! at compile time with `utoipa` — `lumen-host openapi` prints it for client codegen, the
|
||||
//! at compile time with `utoipa` — `punktfunk-host openapi` prints it for client codegen, the
|
||||
//! running server serves it at `/api/v1/openapi.json` plus interactive docs at `/api/docs`,
|
||||
//! and a copy is checked in at `docs/api/openapi.json` (a test fails if it drifts, like the
|
||||
//! cbindgen header).
|
||||
//!
|
||||
//! Security: binds loopback by default. A bearer token (`--mgmt-token` / `LUMEN_MGMT_TOKEN`)
|
||||
//! Security: binds loopback by default. A bearer token (`--mgmt-token` / `PUNKTFUNK_MGMT_TOKEN`)
|
||||
//! is enforced on every `/api/v1` route except `/api/v1/health`, and is mandatory for
|
||||
//! non-loopback binds. The OpenAPI document and docs UI are served unauthenticated (the
|
||||
//! spec is public knowledge — it lives in this repo).
|
||||
@@ -73,7 +73,7 @@ pub async fn run(state: Arc<AppState>, opts: Options) -> Result<()> {
|
||||
let token = opts.token.filter(|t| !t.trim().is_empty());
|
||||
if token.is_none() && !opts.bind.ip().is_loopback() {
|
||||
bail!(
|
||||
"management API bind {} is not loopback — set --mgmt-token (or LUMEN_MGMT_TOKEN) \
|
||||
"management API bind {} is not loopback — set --mgmt-token (or PUNKTFUNK_MGMT_TOKEN) \
|
||||
to expose it beyond this machine",
|
||||
opts.bind
|
||||
);
|
||||
@@ -131,7 +131,7 @@ fn api_router_parts() -> (Router<Arc<MgmtState>>, utoipa::openapi::OpenApi) {
|
||||
.split_for_parts()
|
||||
}
|
||||
|
||||
/// The OpenAPI document as pretty JSON — what `lumen-host openapi` prints and what is
|
||||
/// The OpenAPI document as pretty JSON — what `punktfunk-host openapi` prints and what is
|
||||
/// checked in at `docs/api/openapi.json` for client codegen.
|
||||
pub fn openapi_json() -> String {
|
||||
let (_, api) = api_router_parts();
|
||||
@@ -143,8 +143,8 @@ pub fn openapi_json() -> String {
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
info(
|
||||
title = "lumen management API",
|
||||
description = "Control-plane API for managing a lumen streaming host: host \
|
||||
title = "punktfunk management API",
|
||||
description = "Control-plane API for managing a punktfunk streaming host: host \
|
||||
capabilities, runtime status, paired clients, the pairing PIN flow, \
|
||||
and session control. Authentication: HTTP bearer token, enforced on \
|
||||
every route except `/api/v1/health` when the host is started with a \
|
||||
@@ -191,9 +191,9 @@ struct Health {
|
||||
/// Always `"ok"` when the host responds.
|
||||
#[schema(example = "ok")]
|
||||
status: String,
|
||||
/// `lumen-host` crate version.
|
||||
/// `punktfunk-host` crate version.
|
||||
version: String,
|
||||
/// `lumen-core` C ABI version.
|
||||
/// `punktfunk-core` C ABI version.
|
||||
abi_version: u32,
|
||||
}
|
||||
|
||||
@@ -205,9 +205,9 @@ struct HostInfo {
|
||||
uniqueid: String,
|
||||
/// Best-effort primary LAN IP.
|
||||
local_ip: String,
|
||||
/// `lumen-host` crate version.
|
||||
/// `punktfunk-host` crate version.
|
||||
version: String,
|
||||
/// `lumen-core` C ABI version.
|
||||
/// `punktfunk-core` C ABI version.
|
||||
abi_version: u32,
|
||||
/// GameStream host version advertised to Moonlight clients.
|
||||
app_version: String,
|
||||
@@ -407,7 +407,7 @@ async fn get_health() -> Json<Health> {
|
||||
Json(Health {
|
||||
status: "ok".into(),
|
||||
version: env!("CARGO_PKG_VERSION").into(),
|
||||
abi_version: lumen_core::ABI_VERSION,
|
||||
abi_version: punktfunk_core::ABI_VERSION,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -429,7 +429,7 @@ async fn get_host_info(State(st): State<Arc<MgmtState>>) -> Json<HostInfo> {
|
||||
uniqueid: h.uniqueid.clone(),
|
||||
local_ip: h.local_ip.to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").into(),
|
||||
abi_version: lumen_core::ABI_VERSION,
|
||||
abi_version: punktfunk_core::ABI_VERSION,
|
||||
app_version: APP_VERSION.into(),
|
||||
gfe_version: GFE_VERSION.into(),
|
||||
// Everything NVENC encodes here (mirrors SERVER_CODEC_MODE_SUPPORT = 3843).
|
||||
@@ -717,7 +717,7 @@ mod tests {
|
||||
let (status, body) = send(&app, get_req("/api/v1/health")).await;
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert_eq!(body["status"], "ok");
|
||||
assert_eq!(body["abi_version"], lumen_core::ABI_VERSION);
|
||||
assert_eq!(body["abi_version"], punktfunk_core::ABI_VERSION);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -813,7 +813,7 @@ mod tests {
|
||||
let (status, body) = send(&app, get_req("/api/v1/clients")).await;
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert_eq!(body[0]["fingerprint"], fingerprint);
|
||||
assert_eq!(body[0]["subject"], "CN=lumen");
|
||||
assert_eq!(body[0]["subject"], "CN=punktfunk");
|
||||
|
||||
// Malformed fingerprint → 400.
|
||||
let bad = axum::http::Request::delete("/api/v1/clients/zz")
|
||||
@@ -973,7 +973,7 @@ mod tests {
|
||||
json.trim(),
|
||||
checked_in.trim(),
|
||||
"docs/api/openapi.json is stale — regenerate with: \
|
||||
cargo run -p lumen-host -- openapi > docs/api/openapi.json"
|
||||
cargo run -p punktfunk-host -- openapi > docs/api/openapi.json"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//! The host hot path (plan §7), wiring the platform stages to `lumen_core`:
|
||||
//! The host hot path (plan §7), wiring the platform stages to `punktfunk_core`:
|
||||
//!
|
||||
//! ```text
|
||||
//! capture(dmabuf) → encode(NVENC/VAAPI) → core[FEC+packetize+pace+send]
|
||||
@@ -10,11 +10,11 @@
|
||||
use crate::capture::Capturer;
|
||||
use crate::encode::{EncodedFrame, Encoder};
|
||||
use anyhow::Result;
|
||||
use lumen_core::packet::{FLAG_PIC, FLAG_SOF};
|
||||
use lumen_core::Session;
|
||||
use punktfunk_core::packet::{FLAG_PIC, FLAG_SOF};
|
||||
use punktfunk_core::Session;
|
||||
|
||||
/// Drive one capture→encode→submit step. The real pipeline spawns threads and uses
|
||||
/// bounded channels; this documents the data flow and the `lumen_core` submit contract.
|
||||
/// bounded channels; this documents the data flow and the `punktfunk_core` submit contract.
|
||||
pub fn pump_once(
|
||||
capturer: &mut dyn Capturer,
|
||||
encoder: &mut dyn Encoder,
|
||||
@@ -14,7 +14,7 @@
|
||||
//! consumes the node via [`crate::capture::capture_virtual_output`].
|
||||
|
||||
use anyhow::Result;
|
||||
pub use lumen_core::Mode;
|
||||
pub use punktfunk_core::Mode;
|
||||
use std::os::fd::OwnedFd;
|
||||
|
||||
/// A created virtual output: a PipeWire source to capture, plus an owned keepalive whose drop
|
||||
@@ -46,7 +46,7 @@ pub trait VirtualDisplay: Send {
|
||||
fn create(&mut self, mode: Mode) -> Result<VirtualOutput>;
|
||||
}
|
||||
|
||||
/// Compositors lumen knows how to drive (plan §6).
|
||||
/// Compositors punktfunk knows how to drive (plan §6).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Compositor {
|
||||
/// KWin / Plasma 6 — `zkde_screencast` virtual output.
|
||||
@@ -59,16 +59,18 @@ pub enum Compositor {
|
||||
Gamescope,
|
||||
}
|
||||
|
||||
/// Detect the compositor to drive: `LUMEN_COMPOSITOR` override, else `XDG_CURRENT_DESKTOP`.
|
||||
/// Detect the compositor to drive: `PUNKTFUNK_COMPOSITOR` override, else `XDG_CURRENT_DESKTOP`.
|
||||
pub fn detect() -> Result<Compositor> {
|
||||
if let Ok(v) = std::env::var("LUMEN_COMPOSITOR") {
|
||||
if let Ok(v) = std::env::var("PUNKTFUNK_COMPOSITOR") {
|
||||
return match v.trim().to_ascii_lowercase().as_str() {
|
||||
"kwin" | "kde" | "plasma" => Ok(Compositor::Kwin),
|
||||
"wlroots" | "sway" | "hyprland" | "wlr" => Ok(Compositor::Wlroots),
|
||||
"mutter" | "gnome" => Ok(Compositor::Mutter),
|
||||
"gamescope" => Ok(Compositor::Gamescope),
|
||||
other => {
|
||||
anyhow::bail!("unknown LUMEN_COMPOSITOR '{other}' (kwin|wlroots|mutter|gamescope)")
|
||||
anyhow::bail!(
|
||||
"unknown PUNKTFUNK_COMPOSITOR '{other}' (kwin|wlroots|mutter|gamescope)"
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -86,7 +88,7 @@ pub fn detect() -> Result<Compositor> {
|
||||
Ok(Compositor::Wlroots)
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"could not detect compositor from XDG_CURRENT_DESKTOP='{desktop}'; set LUMEN_COMPOSITOR"
|
||||
"could not detect compositor from XDG_CURRENT_DESKTOP='{desktop}'; set PUNKTFUNK_COMPOSITOR"
|
||||
)
|
||||
}
|
||||
}
|
||||
+11
-10
@@ -35,11 +35,11 @@ impl VirtualDisplay for GamescopeDisplay {
|
||||
|
||||
fn create(&mut self, mode: Mode) -> Result<VirtualOutput> {
|
||||
// Attach to an already-running gamescope (debug / Steam-launched session) instead of
|
||||
// spawning one: LUMEN_GAMESCOPE_NODE=<pipewire node id>.
|
||||
if let Ok(id) = std::env::var("LUMEN_GAMESCOPE_NODE") {
|
||||
// spawning one: PUNKTFUNK_GAMESCOPE_NODE=<pipewire node id>.
|
||||
if let Ok(id) = std::env::var("PUNKTFUNK_GAMESCOPE_NODE") {
|
||||
let node_id: u32 = id
|
||||
.parse()
|
||||
.context("LUMEN_GAMESCOPE_NODE must be a node id")?;
|
||||
.context("PUNKTFUNK_GAMESCOPE_NODE must be a node id")?;
|
||||
tracing::info!(node_id, "gamescope: attaching to existing PipeWire node");
|
||||
return Ok(VirtualOutput {
|
||||
node_id,
|
||||
@@ -54,7 +54,7 @@ impl VirtualDisplay for GamescopeDisplay {
|
||||
let node_id = wait_for_node(Duration::from_secs(15)).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"gamescope PipeWire node did not appear within 15s — gamescope may have failed to \
|
||||
start or headless capture is unsupported on this GPU/driver (see /tmp/lumen-gamescope.log)"
|
||||
start or headless capture is unsupported on this GPU/driver (see /tmp/punktfunk-gamescope.log)"
|
||||
)
|
||||
})?;
|
||||
tracing::info!(
|
||||
@@ -75,16 +75,17 @@ impl VirtualDisplay for GamescopeDisplay {
|
||||
|
||||
/// File where the wrapper below writes gamescope's `LIBEI_SOCKET` (its EIS server socket),
|
||||
/// read by the libei injector to drive input into the nested app. See [`crate::inject`].
|
||||
pub const EI_SOCKET_FILE: &str = "/tmp/lumen-gamescope-ei";
|
||||
pub const EI_SOCKET_FILE: &str = "/tmp/punktfunk-gamescope-ei";
|
||||
|
||||
/// Spawn `gamescope --backend headless -W w -H h -r hz -- <app>`. The app comes from
|
||||
/// `LUMEN_GAMESCOPE_APP` (default a no-op that just keeps gamescope alive — set it to a real
|
||||
/// `PUNKTFUNK_GAMESCOPE_APP` (default a no-op that just keeps gamescope alive — set it to a real
|
||||
/// game/GL app for actual content, e.g. `steam -gamepadui` for the SteamOS-like session).
|
||||
/// stdout/stderr go to `/tmp/lumen-gamescope.log`. The app is launched through a tiny shell
|
||||
/// stdout/stderr go to `/tmp/punktfunk-gamescope.log`. The app is launched through a tiny shell
|
||||
/// wrapper that relays gamescope's `LIBEI_SOCKET` (set for its children) to [`EI_SOCKET_FILE`]
|
||||
/// so the input injector can connect to gamescope's EIS server from outside.
|
||||
fn spawn(w: u32, h: u32, hz: u32) -> Result<Child> {
|
||||
let app = std::env::var("LUMEN_GAMESCOPE_APP").unwrap_or_else(|_| "sleep infinity".to_string());
|
||||
let app =
|
||||
std::env::var("PUNKTFUNK_GAMESCOPE_APP").unwrap_or_else(|_| "sleep infinity".to_string());
|
||||
let _ = std::fs::remove_file(EI_SOCKET_FILE); // stale socket path from a previous session
|
||||
let mut cmd = Command::new("gamescope");
|
||||
cmd.args(["--backend", "headless"])
|
||||
@@ -101,7 +102,7 @@ fn spawn(w: u32, h: u32, hz: u32) -> Result<Child> {
|
||||
.args(app.split_whitespace())
|
||||
// Prefer the NVIDIA GL vendor for the nested session (harmless on a pure-NVIDIA box).
|
||||
.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia");
|
||||
if let Ok(log) = std::fs::File::create("/tmp/lumen-gamescope.log") {
|
||||
if let Ok(log) = std::fs::File::create("/tmp/punktfunk-gamescope.log") {
|
||||
if let Ok(log2) = log.try_clone() {
|
||||
cmd.stdout(Stdio::from(log)).stderr(Stdio::from(log2));
|
||||
}
|
||||
@@ -132,7 +133,7 @@ fn wait_for_node(timeout: Duration) -> Option<u32> {
|
||||
|
||||
/// Parse `stream available on node ID: N` from the spawned gamescope's log (ANSI-colored).
|
||||
fn node_from_log() -> Option<u32> {
|
||||
let log = std::fs::read_to_string("/tmp/lumen-gamescope.log").ok()?;
|
||||
let log = std::fs::read_to_string("/tmp/punktfunk-gamescope.log").ok()?;
|
||||
for line in log.lines().rev() {
|
||||
if let Some(pos) = line.find("stream available on node ID:") {
|
||||
let tail = &line[pos + "stream available on node ID:".len()..];
|
||||
@@ -53,7 +53,7 @@ use zkde::zkde_screencast_unstable_v1::ZkdeScreencastUnstableV1 as Screencast;
|
||||
const POINTER_EMBEDDED: u32 = 2;
|
||||
|
||||
/// The name we give the created output; KWin exposes it to output-management as `Virtual-<name>`.
|
||||
const VOUT_NAME: &str = "lumen";
|
||||
const VOUT_NAME: &str = "punktfunk";
|
||||
|
||||
/// Highest interface version we drive. KWin currently advertises 5; we rely on the `created`
|
||||
/// event (deprecated only since v6) for the node id, so cap the bind at 5.
|
||||
@@ -80,7 +80,7 @@ impl VirtualDisplay for KwinDisplay {
|
||||
let stop_thread = stop.clone();
|
||||
let (width, height) = (mode.width, mode.height);
|
||||
thread::Builder::new()
|
||||
.name("lumen-kwin-vout".into())
|
||||
.name("punktfunk-kwin-vout".into())
|
||||
.spawn(move || virtual_output_thread(width, height, setup_tx, stop_thread))
|
||||
.context("spawn KWin virtual-output thread")?;
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@
|
||||
//!
|
||||
//! Requires a running Mutter (`gnome-shell` session, or `gnome-shell --headless` for the
|
||||
//! headless host) on the session bus. GNOME is detected via `XDG_CURRENT_DESKTOP=GNOME` or
|
||||
//! forced with `LUMEN_COMPOSITOR=mutter`.
|
||||
//! forced with `PUNKTFUNK_COMPOSITOR=mutter`.
|
||||
|
||||
use super::{Mode, VirtualDisplay, VirtualOutput};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
@@ -56,7 +56,7 @@ impl VirtualDisplay for MutterDisplay {
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let stop_thread = stop.clone();
|
||||
thread::Builder::new()
|
||||
.name("lumen-mutter-vout".into())
|
||||
.name("punktfunk-mutter-vout".into())
|
||||
.spawn(move || session_thread(setup_tx, stop_thread))
|
||||
.context("spawn Mutter virtual-output thread")?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Zero-copy capture→encode (plan §9): the PipeWire dmabuf is imported into CUDA via EGL and
|
||||
//! handed straight to NVENC, eliminating the per-frame CPU copies (at 5K the CPU-copy path
|
||||
//! moves ~3.5 GB/s). Opt in with `LUMEN_ZEROCOPY=1`; the CPU-copy path stays the default and
|
||||
//! moves ~3.5 GB/s). Opt in with `PUNKTFUNK_ZEROCOPY=1`; the CPU-copy path stays the default and
|
||||
//! the runtime fallback (foreign-allocator / no-dmabuf / import failure).
|
||||
//!
|
||||
//! Pieces: [`cuda`] (driver-API FFI + the shared `CUcontext` + device buffers), [`egl`] (the
|
||||
@@ -14,9 +14,9 @@ pub mod vulkan;
|
||||
pub use cuda::DeviceBuffer;
|
||||
pub use egl::{DmabufPlane, EglImporter};
|
||||
|
||||
/// Whether the zero-copy path is opted in (`LUMEN_ZEROCOPY` truthy).
|
||||
/// Whether the zero-copy path is opted in (`PUNKTFUNK_ZEROCOPY` truthy).
|
||||
pub fn enabled() -> bool {
|
||||
std::env::var("LUMEN_ZEROCOPY")
|
||||
std::env::var("PUNKTFUNK_ZEROCOPY")
|
||||
.map(|v| matches!(v.trim(), "1" | "true" | "yes" | "on"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
Reference in New Issue
Block a user