rename: lumen → punktfunk, everywhere
ci / rust (push) Has been cancelled

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:
2026-06-10 13:11:59 +00:00
parent b8b23c8fb2
commit bfd64ce871
119 changed files with 1245 additions and 1185 deletions
@@ -0,0 +1,74 @@
//! In-process transport for unit tests and the C ABI harness. Two cross-wired
//! [`LoopbackTransport`]s form a host↔client link, with optional deterministic loss so
//! tests can exercise FEC recovery without a real network.
use super::Transport;
use std::collections::VecDeque;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
/// One direction of the link.
struct Channel {
queue: Mutex<VecDeque<Vec<u8>>>,
/// Drop one of every `drop_period` packets (0 = lossless).
drop_period: u32,
sent: AtomicU64,
dropped: AtomicU64,
}
impl Channel {
fn new(drop_period: u32) -> Arc<Channel> {
Arc::new(Channel {
queue: Mutex::new(VecDeque::new()),
drop_period,
sent: AtomicU64::new(0),
dropped: AtomicU64::new(0),
})
}
}
/// Sends on `tx`, receives on `rx`. Created in cross-wired pairs by [`loopback_pair`].
pub struct LoopbackTransport {
tx: Arc<Channel>,
rx: Arc<Channel>,
}
impl LoopbackTransport {
/// Number of packets this transport's send side has deliberately dropped.
pub fn dropped(&self) -> u64 {
self.tx.dropped.load(Ordering::Relaxed)
}
}
/// Create a connected `(host, client)` pair. `host_drop_period` injects loss on the
/// host→client (video) path; `client_drop_period` on the reverse (input) path.
pub fn loopback_pair(
host_drop_period: u32,
client_drop_period: u32,
) -> (LoopbackTransport, LoopbackTransport) {
let h2c = Channel::new(host_drop_period);
let c2h = Channel::new(client_drop_period);
let host = LoopbackTransport {
tx: h2c.clone(),
rx: c2h.clone(),
};
let client = LoopbackTransport { tx: c2h, rx: h2c };
(host, client)
}
impl Transport for LoopbackTransport {
fn send(&self, packet: &[u8]) -> std::io::Result<()> {
let n = self.tx.sent.fetch_add(1, Ordering::Relaxed);
if self.tx.drop_period != 0 && (n % self.tx.drop_period as u64) == 0 {
// Deterministically drop in flight (the 1st of each `drop_period` group).
self.tx.dropped.fetch_add(1, Ordering::Relaxed);
return Ok(());
}
self.tx.queue.lock().unwrap().push_back(packet.to_vec());
Ok(())
}
fn recv(&self) -> std::io::Result<Option<Vec<u8>>> {
Ok(self.rx.queue.lock().unwrap().pop_front())
}
}
@@ -0,0 +1,15 @@
//! Pluggable packet I/O. The hot path calls [`Transport::send`] / [`Transport::recv`]
//! directly — no async runtime is involved.
mod loopback;
mod udp;
pub use loopback::{loopback_pair, LoopbackTransport};
pub use udp::UdpTransport;
/// A datagram transport. `recv` is non-blocking: it returns `Ok(None)` when no packet
/// is currently available, so the caller (decode/present thread) never blocks here.
pub trait Transport: Send + Sync {
fn send(&self, packet: &[u8]) -> std::io::Result<()>;
fn recv(&self) -> std::io::Result<Option<Vec<u8>>>;
}
@@ -0,0 +1,52 @@
//! Real UDP datagram transport — native sockets, no async runtime.
//!
//! M1 uses one `recv` syscall per packet; the latency budget (§7) calls for
//! `sendmmsg`/UDP-GSO batching to cut syscalls, which is a P2 optimization layered on
//! this same [`Transport`] seam.
use super::Transport;
use crate::packet::MAX_DATAGRAM_BYTES;
use std::net::UdpSocket;
/// Receive buffer size. `Config::validate` bounds `shard_payload` so a well-formed
/// datagram (header + shard + crypto overhead) always fits in [`MAX_DATAGRAM_BYTES`];
/// the `+ 1` byte lets us detect an oversized datagram (a full read) instead of
/// silently truncating it.
const RECV_BUF: usize = MAX_DATAGRAM_BYTES + 1;
pub struct UdpTransport {
socket: UdpSocket,
}
impl UdpTransport {
/// Bind `local` and `connect` to `peer`, so `send`/`recv` need no address and the
/// kernel filters to this peer. Non-blocking, matching the [`Transport`] contract.
pub fn connect(local: &str, peer: &str) -> std::io::Result<Self> {
let socket = UdpSocket::bind(local)?;
socket.connect(peer)?;
socket.set_nonblocking(true)?;
Ok(UdpTransport { socket })
}
}
impl Transport for UdpTransport {
fn send(&self, packet: &[u8]) -> std::io::Result<()> {
self.socket.send(packet)?;
Ok(())
}
fn recv(&self) -> std::io::Result<Option<Vec<u8>>> {
let mut buf = vec![0u8; RECV_BUF];
match self.socket.recv(&mut buf) {
// A read that fills the whole buffer means the datagram was larger than any
// valid packet — drop it rather than hand a truncated, corrupt packet up.
Ok(n) if n >= RECV_BUF => Ok(None),
Ok(n) => {
buf.truncate(n);
Ok(Some(buf))
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(None),
Err(e) => Err(e),
}
}
}