diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index f31d6768..d3ae1fbd 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1,4 +1,4 @@ -# CI for lumen (Gitea Actions, GitHub-Actions-compatible syntax). +# CI for punktfunk (Gitea Actions, GitHub-Actions-compatible syntax). # Adjust `runs-on` to match your runner labels if not using the default ubuntu image. name: ci @@ -34,10 +34,10 @@ jobs: run: cargo test --workspace --locked - name: C ABI harness (standalone link proof) - run: bash crates/lumen-core/tests/c/run.sh + run: bash crates/punktfunk-core/tests/c/run.sh - name: Verify generated header is committed & up to date run: | - cargo build -p lumen-core - git diff --exit-code include/lumen_core.h \ - || (echo "include/lumen_core.h is stale — commit the regenerated header" && exit 1) + cargo build -p punktfunk-core + git diff --exit-code include/punktfunk_core.h \ + || (echo "include/punktfunk_core.h is stale — commit the regenerated header" && exit 1) diff --git a/.gitignore b/.gitignore index 5a465edf..904f3aec 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,5 @@ node_modules/ dist/ # Swift package build artifacts + the locally-built xcframework (rebuild via scripts/build-xcframework.sh) clients/apple/.build/ -clients/apple/LumenCore.xcframework/ +clients/apple/PunktfunkCore.xcframework/ clients/apple/.swiftpm/ diff --git a/CLAUDE.md b/CLAUDE.md index aae50fd5..1c1fa67d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,17 +1,17 @@ -# CLAUDE.md — lumen +# CLAUDE.md — punktfunk Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protocol core -(`lumen-core`) exposed over a C ABI and native clients per platform. Full design: +(`punktfunk-core`) exposed over a C ABI and native clients per platform. Full design: [`docs/implementation-plan.md`](docs/implementation-plan.md). Status table: `README.md`. ## Where the work stands -- **M1 (`lumen-core` + C ABI): complete and hardened.** FEC recovery, loopback-under-loss, +- **M1 (`punktfunk-core` + C ABI): complete and hardened.** FEC recovery, loopback-under-loss, proptests, C ABI harness all green; 13 adversarial-review findings fixed + regression-tested (`a913042`). - **M2 (GameStream host): working end-to-end with a stock Moonlight client.** Validated live on this box: pairing (persists across restarts), serverinfo/applist (app catalog from - `~/.config/lumen/apps.json` → each entry picks a compositor + nested command), RTSP, ENet + `~/.config/punktfunk/apps.json` → each entry picks a compositor + nested command), RTSP, ENet control, audio, and video at the **client's native resolution and refresh** — the host creates a per-session virtual output via per-compositor `VirtualDisplay` backends: **KWin** (`zkde_screencast stream_virtual_output`, needs KWin ≥ 6.5.6 headless; >60 Hz via @@ -26,27 +26,27 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc socket, wlr protocols on Sway) and **gamepads** (uinput X-Box-360 pads + rumble back-channel; validated live — pad created/destroyed with the session). Management REST API + checked-in OpenAPI doc (`mgmt.rs`). -- **M3 (`lumen/1`, the native protocol): full session planes, validated live.** QUIC - control plane (`lumen-core` `quic` feature: Hello{mode}/Welcome{full Config}/Start), data +- **M3 (`punktfunk/1`, the native protocol): full session planes, validated live.** QUIC + control plane (`punktfunk-core` `quic` feature: Hello{mode}/Welcome{full Config}/Start), data plane = the hardened M1 `Session` over raw UDP with **GF(2¹⁶) Leopard FEC + AES-GCM** (inexpressible in GameStream), host creates the native virtual output at the client's requested mode. `m3-host` is a **persistent listener** (sessions back to back; `--max-sessions`). QUIC datagrams carry the side planes, demuxed by first byte: input 0xC8 (incl. **gamepads** — incremental events accumulated into the uinput xpad), **Opus audio** 0xC9 (48 kHz stereo, 5 ms, host→client), **rumble** 0xCA (host→client). **Trust:** - host serves its persistent identity (`~/.config/lumen/cert.pem`, shared with GameStream + host serves its persistent identity (`~/.config/punktfunk/cert.pem`, shared with GameStream pairing) and logs the SHA-256 fingerprint; clients pin it (TOFU on first connect — `endpoint::client_pinned`). Measured on-box at 720p120: 1680/1680 frames, **p50 0.83 ms** - capture→…→reassembled; audio measured live (~200 pkts/s). `lumen-client-rs` is the + capture→…→reassembled; audio measured live (~200 pkts/s). `punktfunk-client-rs` is the working reference client (`--pin`, datagram counters, `--input-test` incl. gamepad). - The embeddable connector (`NativeClient`) exposes it all over the C ABI: `lumen_connect` + The embeddable connector (`NativeClient`) exposes it all over the C ABI: `punktfunk_connect` (pin/TOFU) + `next_au`/`next_audio`/`next_rumble`/`send_input`. ## What's left 1. **M4 — client decode + present: macOS stage 1 done, first light achieved - (2026-06-10).** LumenKit compiles and is tested on macOS (AnnexB → VideoToolbox → - `AVSampleBufferDisplayLayer`, GCMouse/GCKeyboard capture, `LumenClient` app shell); + (2026-06-10).** PunktfunkKit compiles and is tested on macOS (AnnexB → VideoToolbox → + `AVSampleBufferDisplayLayer`, GCMouse/GCKeyboard capture, `PunktfunkClient` app shell); validated live Mac ↔ this box at 720p60 — vkcube on glass, input injected via gamescope EIS. Tests: `swift test` in `clients/apple` (unit + real-codec round trip), `test-loopback.sh` (Swift client vs synthetic m3-host on loopback — runs on macOS), @@ -54,16 +54,16 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc [`clients/apple/README.md`](clients/apple/README.md). Next: stage 2 presenter (`VTDecompressionSession` + `CAMetalLayer` frame pacing), glass-to-glass numbers via `tools/latency-probe` (scaffold), iOS variant. The Linux reference client - (`lumen-client-rs`) gets VAAPI + wgpu on the same connector later. + (`punktfunk-client-rs`) gets VAAPI + wgpu on the same connector later. 2. **Sub-frame pipelining**: overlap encode and transmit within a frame. Requires a direct NVENC SDK wrapper (libavcodec only emits whole AUs) — the next big latency lever (~2–4 ms at high res). -3. **lumen/1 protocol growth**: a PIN-style pairing ceremony on top of fingerprint pinning, +3. **punktfunk/1 protocol growth**: a PIN-style pairing ceremony on top of fingerprint pinning, mid-stream mode renegotiation (the Welcome is one-shot today), concurrent sessions (today: one at a time, extras wait in the accept queue). 4. **M2 polish**: wlroots/Sway `VirtualDisplay` backend (deferred; swaymsg `create_output`), HDR/10-bit/AV1 negotiation, surround audio, reconnect-at-new-mode robustness. -5. **Native clients** (`clients/{apple,android}` scaffolds) consuming `lumen_core.h`. +5. **Native clients** (`clients/{apple,android}` scaffolds) consuming `punktfunk_core.h`. Box one-time setup is complete: udev rule + `input` group (gamepads validated live), gamescope 3.16.22 installed system-wide (no PATH override), gnome-shell installed (Mutter @@ -78,32 +78,32 @@ cargo clippy --workspace --all-targets -- -D warnings cargo fmt --all --check cargo run -p loss-harness # FEC loss-resilience sweep (no network needed) -bash crates/lumen-core/tests/c/run.sh # standalone C-ABI link + round-trip proof +bash crates/punktfunk-core/tests/c/run.sh # standalone C-ABI link + round-trip proof ``` -Generated artifacts are **checked in** and CI fails on drift: `include/lumen_core.h` -(cbindgen from `lumen-core/src/abi.rs`) and `docs/api/openapi.json` (regenerate with -`cargo run -p lumen-host -- openapi > docs/api/openapi.json`; spec lives in `mgmt.rs`). +Generated artifacts are **checked in** and CI fails on drift: `include/punktfunk_core.h` +(cbindgen from `punktfunk-core/src/abi.rs`) and `docs/api/openapi.json` (regenerate with +`cargo run -p punktfunk-host -- openapi > docs/api/openapi.json`; spec lives in `mgmt.rs`). ## Layout ``` -crates/lumen-core/ protocol · FEC · crypto · quic (lumen/1 control plane, feature-gated) -crates/lumen-host/ +crates/punktfunk-core/ protocol · FEC · crypto · quic (punktfunk/1 control plane, feature-gated) +crates/punktfunk-host/ gamestream/ Moonlight compat: nvhttp · pairing · rtsp · control · stream · gamepad · apps vdisplay/{kwin,gamescope,mutter}.rs per-compositor client-sized virtual outputs zerocopy/{egl,cuda,vulkan}.rs dmabuf → CUDA → NVENC (tiled via EGL/GL, LINEAR via Vulkan) inject/{libei,wlr,gamepad}.rs input backends (+ uinput virtual gamepads) capture.rs · encode.rs · audio.rs · m0.rs · m3.rs · mgmt.rs -crates/lumen-client-rs/ lumen/1 reference client (M3 headless; M4 adds decode+present) +crates/punktfunk-client-rs/ punktfunk/1 reference client (M3 headless; M4 adds decode+present) tools/{loss-harness,latency-probe}/ measurement (plan §10) -scripts/ 60-lumen.rules · lumen-host.service · host.env.example · headless/ -include/lumen_core.h generated C header +scripts/ 60-punktfunk.rules · punktfunk-host.service · host.env.example · headless/ +include/punktfunk_core.h generated C header ``` ## Design invariants — do not regress -- **One core, linked everywhere.** Protocol/FEC/crypto live only in `lumen-core`, behind a +- **One core, linked everywhere.** Protocol/FEC/crypto live only in `punktfunk-core`, behind a stable, versioned C ABI. `tokio`/`quinn` exist only behind the `quic` feature (control plane); **no async on the per-frame path** — native threads only. - **Native client resolution, no scaling.** A session gets a virtual output at exactly the @@ -111,7 +111,7 @@ include/lumen_core.h generated C header remote_fd, preferred_mode, keepalive }`, RAII teardown). There is no cross-compositor protocol for this — each compositor keeps its own backend. - **FEC is the wall-breaker.** GF(2⁸) (≤255 shards/block, Moonlight-compatible) and GF(2¹⁶) - Leopard (≤65535 shards/block) — lumen/1 negotiates the latter, removing the ~1 Gbps + Leopard (≤65535 shards/block) — punktfunk/1 negotiates the latter, removing the ~1 Gbps ceiling. - **M1 security hardening stays intact**: reassembler bounds attacker-controlled fields before allocating (`ReassemblerLimits`); AES-GCM per-direction nonce salts + seq-as-AAD; @@ -127,27 +127,26 @@ module — a kernel update silently drops it; reinstall `nvidia-driver-595-open` scanout → KWin `--drm` impossible; everything renders offscreen via `renderD128`. ```sh -# compositor session (shell 1, or the systemd unit in scripts/): -XDG_RUNTIME_DIR=/run/user/1000 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \ -XDG_CURRENT_DESKTOP=KDE KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 \ -kwin_wayland --virtual --width 1920 --height 1080 --no-lockscreen --socket wayland-kde \ - --exit-with-session wev +# compositor session (shell 1, or the systemd unit in scripts/): full headless Plasma. +# The script sets XDG_MENU_PREFIX=plasma- & co. — without it plasmashell runs but the +# launcher menu is EMPTY (no apps, no System Settings). +bash scripts/headless/run-headless-kde.sh 1920x1080 # host (shell 2): -WAYLAND_DISPLAY=wayland-kde XDG_CURRENT_DESKTOP=KDE LUMEN_VIDEO_SOURCE=virtual \ -LUMEN_ZEROCOPY=1 cargo run -rp lumen-host -- serve +WAYLAND_DISPLAY=wayland-kde XDG_CURRENT_DESKTOP=KDE PUNKTFUNK_VIDEO_SOURCE=virtual \ +PUNKTFUNK_ZEROCOPY=1 cargo run -rp punktfunk-host -- serve -# lumen/1 native loopback test (no Moonlight needed; same env as serve, listener persists +# punktfunk/1 native loopback test (no Moonlight needed; same env as serve, listener persists # across sessions — bound it with --max-sessions): -cargo run -rp lumen-host -- m3-host --source virtual --seconds 10 --max-sessions 1 -cargo run -rp lumen-client-rs -- --mode 1280x720x120 --out /tmp/a.h265 --input-test # + --pin HEX +cargo run -rp punktfunk-host -- m3-host --source virtual --seconds 10 --max-sessions 1 +cargo run -rp punktfunk-client-rs -- --mode 1280x720x120 --out /tmp/a.h265 --input-test # + --pin HEX ``` Pinned crate facts: `ashpd` 0.13 + `pipewire` 0.9 (must match ashpd's) + `ffmpeg-next` 8.x -(system FFmpeg 8 / libavcodec 62). Env knobs: `LUMEN_VIDEO_SOURCE=virtual|portal`, -`LUMEN_COMPOSITOR=kwin|gamescope|mutter`, `LUMEN_ZEROCOPY=1`, `LUMEN_GAMESCOPE_APP=...`, -`LUMEN_INPUT_BACKEND=...`, `LUMEN_PERF=1` (per-stage timing), `LUMEN_VIDEO_DROP=N` (FEC -test), `LUMEN_FEC_PCT=N`. +(system FFmpeg 8 / libavcodec 62). Env knobs: `PUNKTFUNK_VIDEO_SOURCE=virtual|portal`, +`PUNKTFUNK_COMPOSITOR=kwin|gamescope|mutter`, `PUNKTFUNK_ZEROCOPY=1`, `PUNKTFUNK_GAMESCOPE_APP=...`, +`PUNKTFUNK_INPUT_BACKEND=...`, `PUNKTFUNK_PERF=1` (per-stage timing), `PUNKTFUNK_VIDEO_DROP=N` (FEC +test), `PUNKTFUNK_FEC_PCT=N`. ## Conventions @@ -155,4 +154,4 @@ test), `LUMEN_FEC_PCT=N`. - Match the surrounding code's comment density and naming. - Commit messages end with the Co-Authored-By trailer (see `git log`). - `pkill` caution on this box: match exact comm names (`pkill -x gamescope-wl`, - `pkill -x lumen-host`) — `pkill -f` self-matches the invoking shell. + `pkill -x punktfunk-host`) — `pkill -f` self-matches the invoking shell. diff --git a/Cargo.lock b/Cargo.lock index fde1b66d..ca874b09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1451,7 +1451,7 @@ checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" name = "loss-harness" version = "0.0.1" dependencies = [ - "lumen-core", + "punktfunk-core", ] [[package]] @@ -1460,90 +1460,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lumen-client-rs" -version = "0.0.1" -dependencies = [ - "anyhow", - "lumen-core", - "quinn", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "lumen-core" -version = "0.0.1" -dependencies = [ - "aes-gcm", - "bytes", - "cbindgen", - "fec-rs", - "proptest", - "quinn", - "rand 0.9.4", - "rcgen", - "reed-solomon-simd", - "rustls", - "rustls-pki-types", - "sha2", - "thiserror 2.0.18", - "tokio", - "tracing", - "zerocopy", - "zeroize", -] - -[[package]] -name = "lumen-host" -version = "0.0.1" -dependencies = [ - "aes", - "aes-gcm", - "anyhow", - "ash", - "ashpd", - "axum", - "axum-server", - "cbc", - "ffmpeg-next", - "futures-util", - "hex", - "http-body-util", - "khronos-egl", - "libc", - "lumen-core", - "mdns-sd", - "opus", - "pipewire", - "quinn", - "rand 0.8.6", - "rcgen", - "reis", - "rsa", - "rustls", - "rustls-pemfile", - "rusty_enet", - "serde", - "serde_json", - "sha2", - "tokio", - "tower", - "tracing", - "tracing-subscriber", - "utoipa", - "utoipa-axum", - "utoipa-scalar", - "wayland-backend", - "wayland-client", - "wayland-protocols-misc", - "wayland-protocols-wlr", - "wayland-scanner", - "x509-parser", - "xkbcommon", -] - [[package]] name = "matchers" version = "0.2.0" @@ -1981,6 +1897,90 @@ dependencies = [ "unarray", ] +[[package]] +name = "punktfunk-client-rs" +version = "0.0.1" +dependencies = [ + "anyhow", + "punktfunk-core", + "quinn", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "punktfunk-core" +version = "0.0.1" +dependencies = [ + "aes-gcm", + "bytes", + "cbindgen", + "fec-rs", + "proptest", + "quinn", + "rand 0.9.4", + "rcgen", + "reed-solomon-simd", + "rustls", + "rustls-pki-types", + "sha2", + "thiserror 2.0.18", + "tokio", + "tracing", + "zerocopy", + "zeroize", +] + +[[package]] +name = "punktfunk-host" +version = "0.0.1" +dependencies = [ + "aes", + "aes-gcm", + "anyhow", + "ash", + "ashpd", + "axum", + "axum-server", + "cbc", + "ffmpeg-next", + "futures-util", + "hex", + "http-body-util", + "khronos-egl", + "libc", + "mdns-sd", + "opus", + "pipewire", + "punktfunk-core", + "quinn", + "rand 0.8.6", + "rcgen", + "reis", + "rsa", + "rustls", + "rustls-pemfile", + "rusty_enet", + "serde", + "serde_json", + "sha2", + "tokio", + "tower", + "tracing", + "tracing-subscriber", + "utoipa", + "utoipa-axum", + "utoipa-scalar", + "wayland-backend", + "wayland-client", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "x509-parser", + "xkbcommon", +] + [[package]] name = "quick-error" version = "1.2.3" diff --git a/Cargo.toml b/Cargo.toml index fc3682d2..fbb8d1d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [workspace] resolver = "2" members = [ - "crates/lumen-core", - "crates/lumen-host", - "crates/lumen-client-rs", + "crates/punktfunk-core", + "crates/punktfunk-host", + "crates/punktfunk-client-rs", "tools/latency-probe", "tools/loss-harness", ] @@ -14,15 +14,15 @@ edition = "2021" rust-version = "1.82" license = "MIT OR Apache-2.0" authors = ["unom"] -repository = "https://git.unom.io/unom/lumen" +repository = "https://git.unom.io/unom/punktfunk" [profile.release] opt-level = 3 lto = "thin" codegen-units = 1 -# NOTE: deliberately NOT `panic = "abort"`. lumen-core ships as a cdylib/staticlib into +# NOTE: deliberately NOT `panic = "abort"`. punktfunk-core ships as a cdylib/staticlib into # third-party apps (Swift/Kotlin/C) and its C ABI catches panics at the boundary -# (`catch_unwind` → `LumenStatus::Panic`). `panic = "abort"` would make that guard a +# (`catch_unwind` → `PunktfunkStatus::Panic`). `panic = "abort"` would make that guard a # no-op and let a stray panic abort the embedding application. Unwinding keeps the # documented isolation guarantee real. diff --git a/README.md b/README.md index df39fc36..770cdc63 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# lumen +# punktfunk *A ground-up low-latency desktop streaming stack, built Linux-first, with a shared Rust protocol core and native clients per platform.* -`lumen` is a placeholder codename. The bet: ship a **Linux virtual-display streaming +`punktfunk` is a placeholder codename. The bet: ship a **Linux virtual-display streaming host** that speaks the existing Moonlight protocol (every Moonlight/Artemis client works day one), then break the ~1 Gbps FEC wall with a **GF(2¹⁶) Leopard-RS** transport as a negotiated extension. See [`docs/implementation-plan.md`](docs/implementation-plan.md). @@ -12,18 +12,18 @@ negotiated extension. See [`docs/implementation-plan.md`](docs/implementation-pl | Milestone | State | |-----------|-------| -| **M1 — `lumen-core` + C ABI** | ✅ done & tested (FEC, packetization, crypto, session, `lumen_core.h`) | -| **M0 — pipeline spike** (wlroots→PipeWire→NVENC→file→`lumen-core`) | ✅ done & verified on NVIDIA (RTX 5070 Ti / driver 595) | +| **M1 — `punktfunk-core` + C ABI** | ✅ done & tested (FEC, packetization, crypto, session, `punktfunk_core.h`) | +| **M0 — pipeline spike** (wlroots→PipeWire→NVENC→file→`punktfunk-core`) | ✅ done & verified on NVIDIA (RTX 5070 Ti / driver 595) | | M2 — P1 host → stock Moonlight | 🟡 capture+encode landed in M0; pairing/RTSP/vdisplay pending | | M3 — measurement harness | 🟡 `tools/loss-harness` runs; `latency-probe` scaffolded | -| M4 — P2 transport + Rust client | 🟡 GF(2¹⁶) core done; `lumen-client-rs` scaffolded | -| M5 — Apple client | 🟡 macOS first light: HEVC on glass + input over `lumen/1` (`clients/apple`) | +| M4 — P2 transport + Rust client | 🟡 GF(2¹⁶) core done; `punktfunk-client-rs` scaffolded | +| M5 — Apple client | 🟡 macOS first light: HEVC on glass + input over `punktfunk/1` (`clients/apple`) | -`lumen-core` is complete and verified: it builds and its full test suite (FEC recovery, +`punktfunk-core` is complete and verified: it builds and its full test suite (FEC recovery, loopback round-trip under loss, property tests, and a **C ABI harness**) passes on -macOS/aarch64. **M0 is done:** `lumen-host` captures a headless wlroots output via the +macOS/aarch64. **M0 is done:** `punktfunk-host` captures a headless wlroots output via the ScreenCast portal + PipeWire, encodes it with NVENC, writes a playable H.265 file, and -round-trips every access unit through a `lumen_core` host→client session (see +round-trips every access unit through a `punktfunk_core` host→client session (see `docs/linux-setup.md`). M2 is in flight: the GameStream control plane (`gamestream/`) and the management REST API (`mgmt.rs`, OpenAPI spec in `docs/api/`) are implemented; the remaining Linux host backends (KWin/Mutter virtual displays, libei input) are @@ -33,11 +33,11 @@ remaining Linux host backends (KWin/Mutter virtual displays, libei input) are ``` crates/ - lumen-core/ protocol · FEC · pacing · crypto — the C ABI (lib + cdylib + staticlib) - lumen-host/ Linux host: vdisplay · capture · encode · inject · gamestream · mgmt - lumen-client-rs/ reference client (M4): VAAPI decode + wgpu present -clients/{apple,android}/ native client scaffolds (import lumen_core.h) -include/lumen_core.h cbindgen-generated C header (checked in) + punktfunk-core/ protocol · FEC · pacing · crypto — the C ABI (lib + cdylib + staticlib) + punktfunk-host/ Linux host: vdisplay · capture · encode · inject · gamestream · mgmt + punktfunk-client-rs/ reference client (M4): VAAPI decode + wgpu present +clients/{apple,android}/ native client scaffolds (import punktfunk_core.h) +include/punktfunk_core.h cbindgen-generated C header (checked in) tools/{latency-probe,loss-harness}/ measurement (plan §10) docs/implementation-plan.md ``` @@ -50,16 +50,16 @@ cargo test --workspace # unit + loopback + proptest + C ABI harness cargo clippy --workspace --all-targets cargo run -p loss-harness # FEC loss-resilience sweep (no network needed) -bash crates/lumen-core/tests/c/run.sh # standalone C-ABI link+round-trip proof +bash crates/punktfunk-core/tests/c/run.sh # standalone C-ABI link+round-trip proof ``` -The C header regenerates from `crates/lumen-core/src/abi.rs` on every build (cbindgen via -`build.rs`) into `include/lumen_core.h`. +The C header regenerates from `crates/punktfunk-core/src/abi.rs` on every build (cbindgen via +`build.rs`) into `include/punktfunk_core.h`. ## Design invariants -- **One core, linked everywhere.** Protocol/FEC/crypto/pacing live in `lumen-core` exactly - once, exposed over a stable, versioned C ABI (`lumen_abi_version()`, `LumenConfig` +- **One core, linked everywhere.** Protocol/FEC/crypto/pacing live in `punktfunk-core` exactly + once, exposed over a stable, versioned C ABI (`punktfunk_abi_version()`, `PunktfunkConfig` carries its own `struct_size`). - **No async on the hot path.** The per-frame pipeline uses native threads only; `tokio`/`quinn` are gated behind the off-by-default `quic` feature (control plane only). diff --git a/clients/android/README.md b/clients/android/README.md index 49dc4083..ec6a31bd 100644 --- a/clients/android/README.md +++ b/clients/android/README.md @@ -1,18 +1,18 @@ -# lumen Android client (later) +# punktfunk Android client (later) -Kotlin UI + MediaCodec (decode) + a thin JNI layer over the `lumen-core` C ABI. +Kotlin UI + MediaCodec (decode) + a thin JNI layer over the `punktfunk-core` C ABI. ## Wiring 1. Build the core as a shared library per Android ABI: ```sh rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android - cargo build -p lumen-core --release --target aarch64-linux-android # liblumen_core.so + cargo build -p punktfunk-core --release --target aarch64-linux-android # libpunktfunk_core.so ``` (Use `cargo-ndk` to handle the NDK toolchain/linker.) -2. JNI shim: small C/Rust glue mapping `lumen_*` to Kotlin `external fun`s, bundling - `liblumen_core.so` into the APK's `jniLibs/`. -3. Kotlin: client `LumenSession` → `lumen_client_poll_frame` on a decode thread → feed +2. JNI shim: small C/Rust glue mapping `punktfunk_*` to Kotlin `external fun`s, bundling + `libpunktfunk_core.so` into the APK's `jniLibs/`. +3. Kotlin: client `PunktfunkSession` → `punktfunk_client_poll_frame` on a decode thread → feed `MediaCodec` → render to a `SurfaceView` aligned to the display refresh. ## Status diff --git a/clients/apple/Package.swift b/clients/apple/Package.swift index 3fa0316d..52125baa 100644 --- a/clients/apple/Package.swift +++ b/clients/apple/Package.swift @@ -1,21 +1,21 @@ // swift-tools-version: 5.9 -// LumenKit — Swift wrapper around the lumen-core C ABI (lumen/1 client connector) plus the -// SwiftUI/VideoToolbox presentation layer. Build LumenCore.xcframework first: +// PunktfunkKit — Swift wrapper around the punktfunk-core C ABI (punktfunk/1 client connector) plus the +// SwiftUI/VideoToolbox presentation layer. Build PunktfunkCore.xcframework first: // bash ../../scripts/build-xcframework.sh (on a Mac; see README.md) import PackageDescription let package = Package( - name: "LumenKit", + name: "PunktfunkKit", platforms: [.macOS(.v14), .iOS(.v17)], products: [ - .library(name: "LumenKit", targets: ["LumenKit"]), - .executable(name: "LumenClient", targets: ["LumenClient"]), + .library(name: "PunktfunkKit", targets: ["PunktfunkKit"]), + .executable(name: "PunktfunkClient", targets: ["PunktfunkClient"]), ], targets: [ - .binaryTarget(name: "LumenCore", path: "LumenCore.xcframework"), + .binaryTarget(name: "PunktfunkCore", path: "PunktfunkCore.xcframework"), .target( - name: "LumenKit", - dependencies: ["LumenCore"], + name: "PunktfunkKit", + dependencies: ["PunktfunkCore"], linkerSettings: [ // Rust staticlib system deps. .linkedFramework("Security"), @@ -23,8 +23,8 @@ let package = Package( .linkedLibrary("resolv"), ] ), - // Development app shell (swift run LumenClient): connect form → stream + input. - .executableTarget(name: "LumenClient", dependencies: ["LumenKit"]), - .testTarget(name: "LumenKitTests", dependencies: ["LumenKit"]), + // Development app shell (swift run PunktfunkClient): connect form → stream + input. + .executableTarget(name: "PunktfunkClient", dependencies: ["PunktfunkKit"]), + .testTarget(name: "PunktfunkKitTests", dependencies: ["PunktfunkKit"]), ] ) diff --git a/clients/apple/README.md b/clients/apple/README.md index 3325d788..09c89604 100644 --- a/clients/apple/README.md +++ b/clients/apple/README.md @@ -1,31 +1,31 @@ -# lumen Apple client (SwiftUI) +# punktfunk Apple client (SwiftUI) -The native macOS/iOS client for **`lumen/1`** (the post-GameStream protocol). All +The native macOS/iOS client for **`punktfunk/1`** (the post-GameStream protocol). All networking/protocol work — QUIC control plane, UDP data plane, GF(2¹⁶) FEC, AES-GCM, input datagrams, Opus audio, cert pinning — lives in the shared Rust core (statically -linked as `LumenCore.xcframework`); this package is the Swift shell: decode +linked as `PunktfunkCore.xcframework`); this package is the Swift shell: decode (VideoToolbox), present (SwiftUI), input capture. ## Status — first light achieved (2026-06-10) Validated live, Mac ↔ Linux box over the LAN: gamescope virtual output → NVENC HEVC → -`lumen/1` (GF(2¹⁶) FEC + AES-GCM over UDP, QUIC control) → VideoToolbox → +`punktfunk/1` (GF(2¹⁶) FEC + AES-GCM over UDP, QUIC control) → VideoToolbox → `AVSampleBufferDisplayLayer` on glass at 1280×720@60, with mouse/keyboard flowing back as QUIC datagrams into the host's gamescope EIS injector (thousands of events injected during the session). Headless variant of the same proof: `RemoteFirstLightTests` decoded 60/60 received AUs spanning 983 ms of host capture clock. -The connector underneath (`lumen_core::client::NativeClient` over the C ABI) carries the +The connector underneath (`punktfunk_core::client::NativeClient` over the C ABI) carries the full session: video AUs, **Opus audio** (`nextAudio()`), **rumble** (`nextRumble()`), input incl. gamepads, and **cert pinning + TOFU** (`pinSHA256:`/`hostFingerprint`) — see `m3.rs::tests::c_abi_connection_roundtrip` (three sequential sessions: TOFU, pinned -reconnect, wrong-pin rejection). The host (`lumen-host m3-host`) is a persistent listener: +reconnect, wrong-pin rejection). The host (`punktfunk-host m3-host`) is a persistent listener: reconnect at will during development. What's here, all compiled and tested on macOS (Xcode 26.5 / Swift 6.3): -- **`LumenKit`** (library) - - `LumenConnection.swift` — wrapper over the C ABI. AUs/audio are copied into `Data` +- **`PunktfunkKit`** (library) + - `PunktfunkConnection.swift` — wrapper over the C ABI. AUs/audio are copied into `Data` (the C pointer is only valid until the next call of the same kind). `close()` is safe from any thread: per-plane locks enforce the C contract ("never close with a `next_au`/`next_audio` in flight") instead of leaving it to callers. Pinning + TOFU @@ -39,7 +39,7 @@ What's here, all compiled and tested on macOS (Xcode 26.5 / Swift 6.3): `vk_to_evdev` consumes Windows VKs), with fractional-delta accumulation so sub-pixel motion isn't truncated away. Buttons use GameStream ids (1=left … 5=X2); scroll is WHEEL_DELTA(120)-scaled. -- **`LumenClient`** (development app shell): connect form → stream + input, fps/Mb-s HUD. +- **`PunktfunkClient`** (development app shell): connect form → stream + input, fps/Mb-s HUD. (Audio playback and gamepad capture are not wired into the app yet — the connector surface is there; see notes 5–6.) - **Tests** (`swift test`): byte-level Annex-B units; a real-codec round trip @@ -51,29 +51,29 @@ What's here, all compiled and tested on macOS (Xcode 26.5 / Swift 6.3): ```sh rustup target add aarch64-apple-darwin x86_64-apple-darwin -bash scripts/build-xcframework.sh # → clients/apple/LumenCore.xcframework +bash scripts/build-xcframework.sh # → clients/apple/PunktfunkCore.xcframework cd clients/apple swift build && swift test # loopback/remote tests self-skip without a host -swift run LumenClient # the app; or open Package.swift in Xcode +swift run PunktfunkClient # the app; or open Package.swift in Xcode -bash test-loopback.sh # full loopback proof: builds lumen-host +bash test-loopback.sh # full loopback proof: builds punktfunk-host # (synthetic source — runs on macOS), streams # byte-verified frames into the Swift client # against the real host (Linux box, see CLAUDE.md "Running on this box") — m3-host is a # persistent listener, reconnect at will: -# LUMEN_COMPOSITOR=gamescope LUMEN_GAMESCOPE_APP=vkcube LUMEN_ZEROCOPY=1 \ -# cargo run -rp lumen-host -- m3-host --source virtual --seconds 60 -LUMEN_REMOTE_HOST= swift test --filter RemoteFirstLightTests # headless -LUMEN_AUTOCONNECT= LUMEN_MODE=1280x720x60 swift run LumenClient # on glass +# PUNKTFUNK_COMPOSITOR=gamescope PUNKTFUNK_GAMESCOPE_APP=vkcube PUNKTFUNK_ZEROCOPY=1 \ +# cargo run -rp punktfunk-host -- m3-host --source virtual --seconds 60 +PUNKTFUNK_REMOTE_HOST= swift test --filter RemoteFirstLightTests # headless +PUNKTFUNK_AUTOCONNECT= PUNKTFUNK_MODE=1280x720x60 swift run PunktfunkClient # on glass ``` ## Notes for whoever picks this up next 1. **cbindgen import quirk** (the predicted "small compile fixes", now fixed): the - C17-compatible header spells `LumenStatus`/`LumenInputKind` as integer typedefs while + C17-compatible header spells `PunktfunkStatus`/`PunktfunkInputKind` as integer typedefs while the enum *constants* import into Swift as a distinct same-named type — bridge with - `.rawValue` (see the top of `LumenConnection.swift`). Don't fight the generated header. + `.rawValue` (see the top of `PunktfunkConnection.swift`). Don't fight the generated header. 2. **ABI contract**: one video pump thread per connection, plus optionally one *separate* audio drain thread for `nextAudio()`/`nextRumble()` (the core keeps per-plane borrow slots, so the planes never alias); `send()` is enqueue-only and safe alongside all of @@ -91,7 +91,7 @@ LUMEN_AUTOCONNECT= LUMEN_MODE=1280x720x60 swift run LumenClient # on gla `AVAudioEngine` source node; conceal gaps (drop/dup) rather than blocking — the Rust side buffers 320 ms and drops the newest packet when the puller lags. Wall-clock `ptsNs` shares the host clock with video AUs for A/V sync. Wiring this into - `LumenClient` is the next app-side task. + `PunktfunkClient` is the next app-side task. 6. **Gamepads**: `GCController` → `.gamepadButton(...)`/`.gamepadAxis(...)` events (wire contract documented on the constructors; the host accumulates them into a virtual Xbox 360 pad). Poll `nextRumble()` and feed `GCDeviceHaptics` for force feedback. @@ -99,7 +99,7 @@ LUMEN_AUTOCONNECT= LUMEN_MODE=1280x720x60 swift run LumenClient # on gla 7. **Trust**: connect once with `pinSHA256: nil` (TOFU), persist `hostFingerprint` keyed by host, pass it on every later connect — a mismatch throws `.connectFailed`. The host logs its fingerprint at startup ("clients pin this fingerprint") for out-of-band - verification UX; a PIN-style pairing ceremony is a later lumen-core task. `LumenClient` + verification UX; a PIN-style pairing ceremony is a later punktfunk-core task. `PunktfunkClient` doesn't persist fingerprints yet — add it alongside the "add host" UX. 8. **Input capture caveats** (stage 1): GC handlers only fire while the app has focus — on focus loss `InputCapture` auto-releases everything still held (keys + buttons) so diff --git a/clients/apple/Sources/LumenClient/ContentView.swift b/clients/apple/Sources/PunktfunkClient/ContentView.swift similarity index 83% rename from clients/apple/Sources/LumenClient/ContentView.swift rename to clients/apple/Sources/PunktfunkClient/ContentView.swift index 58e2670f..ae448b57 100644 --- a/clients/apple/Sources/LumenClient/ContentView.swift +++ b/clients/apple/Sources/PunktfunkClient/ContentView.swift @@ -1,16 +1,16 @@ // Connect form ⇄ live stream. Stage-1 UX: pick host + mode, see frames, type/aim. import AppKit -import LumenKit +import PunktfunkKit import SwiftUI struct ContentView: View { @StateObject private var model = SessionModel() - @AppStorage("lumen.host") private var host = "192.168.1.70" - @AppStorage("lumen.port") private var port = 9777 - @AppStorage("lumen.width") private var width = 1920 - @AppStorage("lumen.height") private var height = 1080 - @AppStorage("lumen.hz") private var hz = 60 + @AppStorage("punktfunk.host") private var host = "192.168.1.70" + @AppStorage("punktfunk.port") private var port = 9777 + @AppStorage("punktfunk.width") private var width = 1920 + @AppStorage("punktfunk.height") private var height = 1080 + @AppStorage("punktfunk.hz") private var hz = 60 var body: some View { Group { @@ -24,17 +24,17 @@ struct ContentView: View { .onDisappear { model.disconnect() } // window closed mid-session (Cmd+N spawns more) } - /// Development hook: LUMEN_AUTOCONNECT=host[:port] connects immediately at the saved - /// (or LUMEN_MODE=WxHxHz) mode — lets scripts drive first-light runs. (IPv4/hostname + /// Development hook: PUNKTFUNK_AUTOCONNECT=host[:port] connects immediately at the saved + /// (or PUNKTFUNK_MODE=WxHxHz) mode — lets scripts drive first-light runs. (IPv4/hostname /// only; an IPv6 literal would need bracket parsing.) private func autoConnectIfAsked() { - guard let target = ProcessInfo.processInfo.environment["LUMEN_AUTOCONNECT"], + guard let target = ProcessInfo.processInfo.environment["PUNKTFUNK_AUTOCONNECT"], !target.isEmpty, model.connection == nil, !model.connecting else { return } let parts = target.split(separator: ":") host = String(parts[0]) if parts.count == 2, let p = Int(parts[1]) { port = p } - if let mode = ProcessInfo.processInfo.environment["LUMEN_MODE"] { + if let mode = ProcessInfo.processInfo.environment["PUNKTFUNK_MODE"] { let dims = mode.split(separator: "x").compactMap { Int($0) } if dims.count == 3 { width = dims[0] @@ -48,7 +48,7 @@ struct ContentView: View { hz: UInt32(clamping: hz)) } - private func stream(_ conn: LumenConnection) -> some View { + private func stream(_ conn: PunktfunkConnection) -> some View { StreamView( connection: conn, onFrame: { [meter = model.meter] au in meter.note(byteCount: au.data.count) }, @@ -61,7 +61,7 @@ struct ContentView: View { .background(Color.black) } - private func hud(_ conn: LumenConnection) -> some View { + private func hud(_ conn: PunktfunkConnection) -> some View { VStack(alignment: .trailing, spacing: 4) { Text("\(conn.width)×\(conn.height)@\(conn.refreshHz) \(model.fps) fps \(model.mbps, specifier: "%.1f") Mb/s") .font(.system(.caption, design: .monospaced)) @@ -76,7 +76,7 @@ struct ContentView: View { private var connectForm: some View { VStack(spacing: 14) { - Text("lumen").font(.largeTitle.weight(.semibold)) + Text("punktfunk").font(.largeTitle.weight(.semibold)) Form { TextField("Host", text: $host) TextField("Port", value: $port, format: .number.grouping(.never)) diff --git a/clients/apple/Sources/LumenClient/LumenClientApp.swift b/clients/apple/Sources/PunktfunkClient/PunktfunkClientApp.swift similarity index 82% rename from clients/apple/Sources/LumenClient/LumenClientApp.swift rename to clients/apple/Sources/PunktfunkClient/PunktfunkClientApp.swift index 42f24810..308c8abc 100644 --- a/clients/apple/Sources/LumenClient/LumenClientApp.swift +++ b/clients/apple/Sources/PunktfunkClient/PunktfunkClientApp.swift @@ -1,15 +1,15 @@ -// LumenClient — development app shell around LumenKit (swift run LumenClient). +// PunktfunkClient — development app shell around PunktfunkKit (swift run PunktfunkClient). // Connect form → StreamView (AVSampleBufferDisplayLayer HEVC) + InputCapture. import AppKit import SwiftUI @main -struct LumenClientApp: App { +struct PunktfunkClientApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate var body: some Scene { - WindowGroup("lumen") { + WindowGroup("punktfunk") { ContentView() } } diff --git a/clients/apple/Sources/LumenClient/SessionModel.swift b/clients/apple/Sources/PunktfunkClient/SessionModel.swift similarity index 90% rename from clients/apple/Sources/LumenClient/SessionModel.swift rename to clients/apple/Sources/PunktfunkClient/SessionModel.swift index 598fe62a..aa9942f6 100644 --- a/clients/apple/Sources/LumenClient/SessionModel.swift +++ b/clients/apple/Sources/PunktfunkClient/SessionModel.swift @@ -2,7 +2,7 @@ // pump-thread → main-actor stats relay. import Foundation -import LumenKit +import PunktfunkKit import SwiftUI /// Pump-thread-side frame counters; a 1 Hz main-actor timer drains them into @Published @@ -35,7 +35,7 @@ final class FrameMeter: @unchecked Sendable { @MainActor final class SessionModel: ObservableObject { - @Published var connection: LumenConnection? + @Published var connection: PunktfunkConnection? @Published var connecting = false @Published var errorMessage: String? @Published var fps = 0 @@ -51,8 +51,8 @@ final class SessionModel: ObservableObject { connecting = true errorMessage = nil Task.detached(priority: .userInitiated) { - // LumenConnection.init blocks on the QUIC handshake — keep it off the main actor. - let result = Result { try LumenConnection( + // PunktfunkConnection.init blocks on the QUIC handshake — keep it off the main actor. + let result = Result { try PunktfunkConnection( host: host, port: port, width: width, height: height, refreshHz: hz) } await MainActor.run { [weak self] in guard let self else { return } @@ -64,7 +64,7 @@ final class SessionModel: ObservableObject { self.startStatsTimer() case .failure: self.errorMessage = "Connection failed — is the host running? " + - "(lumen-host m3-host on \(host):\(port))" + "(punktfunk-host m3-host on \(host):\(port))" } } } @@ -92,7 +92,7 @@ final class SessionModel: ObservableObject { errorMessage = "Session ended by host." } - private func startInput(_ conn: LumenConnection) { + private func startInput(_ conn: PunktfunkConnection) { let capture = InputCapture(connection: conn) capture.start() inputCapture = capture diff --git a/clients/apple/Sources/LumenKit/AnnexB.swift b/clients/apple/Sources/PunktfunkKit/AnnexB.swift similarity index 98% rename from clients/apple/Sources/LumenKit/AnnexB.swift rename to clients/apple/Sources/PunktfunkKit/AnnexB.swift index 00a5414a..ed9436de 100644 --- a/clients/apple/Sources/LumenKit/AnnexB.swift +++ b/clients/apple/Sources/PunktfunkKit/AnnexB.swift @@ -1,6 +1,6 @@ // Annex-B HEVC → CoreMedia plumbing. // -// The lumen host emits Annex-B access units with in-band VPS/SPS/PPS on every IDR +// The punktfunk host emits Annex-B access units with in-band VPS/SPS/PPS on every IDR // (deliberately — the client needs no out-of-band extradata). VideoToolbox wants the AVCC // flavor instead: a CMVideoFormatDescription built from the parameter sets, and sample // buffers whose NALs are 4-byte-length-prefixed. This file converts between the two. diff --git a/clients/apple/Sources/LumenKit/InputCapture.swift b/clients/apple/Sources/PunktfunkKit/InputCapture.swift similarity index 97% rename from clients/apple/Sources/LumenKit/InputCapture.swift rename to clients/apple/Sources/PunktfunkKit/InputCapture.swift index 5685e0ab..54deb90f 100644 --- a/clients/apple/Sources/LumenKit/InputCapture.swift +++ b/clients/apple/Sources/PunktfunkKit/InputCapture.swift @@ -1,4 +1,4 @@ -// Input capture → lumen/1 datagrams, via the GameController framework. +// Input capture → punktfunk/1 datagrams, via the GameController framework. // // GCMouse delivers RAW deltas (not the accelerated cursor) — exactly what the host-side // injector expects for relative motion. GCKeyboard gives HID keycodes which we map to the @@ -22,12 +22,12 @@ import AppKit import Foundation import GameController -import LumenCore +import PunktfunkCore public final class InputCapture { private static weak var activeCapture: InputCapture? - private let connection: LumenConnection + private let connection: PunktfunkConnection private var observers: [NSObjectProtocol] = [] private var mice: [GCMouse] = [] private var keyboards: [GCKeyboard] = [] @@ -40,7 +40,7 @@ public final class InputCapture { private var pressedVKs: Set = [] private var pressedButtons: Set = [] - public init(connection: LumenConnection) { + public init(connection: PunktfunkConnection) { self.connection = connection } @@ -183,7 +183,7 @@ public final class InputCapture { } /// HID usage (GCKeyCode raw) → Windows VK (the host maps VK → evdev; every VK emitted - /// here exists in lumen-host/src/inject.rs::vk_to_evdev — extend the two together). + /// here exists in punktfunk-host/src/inject.rs::vk_to_evdev — extend the two together). static let hidToVK: [Int: UInt32] = { var m: [Int: UInt32] = [:] // a–z: HID 0x04..0x1D → VK 'A'..'Z'. diff --git a/clients/apple/Sources/LumenKit/LumenConnection.swift b/clients/apple/Sources/PunktfunkKit/PunktfunkConnection.swift similarity index 74% rename from clients/apple/Sources/LumenKit/LumenConnection.swift rename to clients/apple/Sources/PunktfunkKit/PunktfunkConnection.swift index 7dc6086e..820130cd 100644 --- a/clients/apple/Sources/LumenKit/LumenConnection.swift +++ b/clients/apple/Sources/PunktfunkKit/PunktfunkConnection.swift @@ -1,6 +1,6 @@ -// Swift wrapper around the lumen-core C ABI's lumen/1 connection API. +// Swift wrapper around the punktfunk-core C ABI's punktfunk/1 connection API. // -// Threading contract (mirrors the C header): one LumenConnection is pumped from a single +// Threading contract (mirrors the C header): one PunktfunkConnection is pumped from a single // video thread via nextAU(); nextAudio()/nextRumble() may each run on their own (single) // drain thread — the core keeps per-plane borrow slots, so the planes never alias; // send() is enqueue-only and safe alongside all of them. The pointers inside an AU/audio @@ -18,14 +18,14 @@ // close, the pull methods throw `.closed` and the threads unwind on their own. import Foundation -import LumenCore +import PunktfunkCore // cbindgen's C17-compatible header spells the typedefs as plain integers -// (`typedef int32_t LumenStatus`, `typedef uint8_t LumenInputKind`) while the enum +// (`typedef int32_t PunktfunkStatus`, `typedef uint8_t PunktfunkInputKind`) while the enum // constants import as a distinct same-named Swift type — bridge by raw value once here. -private let statusOK: Int32 = LUMEN_STATUS_OK.rawValue -private let statusNoFrame: Int32 = LUMEN_STATUS_NO_FRAME.rawValue -private let statusClosed: Int32 = LUMEN_STATUS_CLOSED.rawValue +private let statusOK: Int32 = PUNKTFUNK_STATUS_OK.rawValue +private let statusNoFrame: Int32 = PUNKTFUNK_STATUS_NO_FRAME.rawValue +private let statusClosed: Int32 = PUNKTFUNK_STATUS_CLOSED.rawValue /// One reassembled, FEC-recovered, decrypted access unit (Annex-B HEVC from the host). public struct AccessUnit: Sendable { @@ -43,7 +43,7 @@ public struct AudioPacket: Sendable { public let seq: UInt32 } -public enum LumenClientError: Error { +public enum PunktfunkClientError: Error { /// Connect failed — wrong host/port, timeout, or a certificate-pin mismatch. case connectFailed /// `pinSHA256` was non-nil but not exactly 32 bytes. Failing closed: connecting @@ -53,7 +53,7 @@ public enum LumenClientError: Error { case status(Int32) } -public final class LumenConnection { +public final class PunktfunkConnection { private var handle: OpaquePointer? /// Set by close() before it contends for the plane locks: the pullers see it at their /// next poll boundary and exit, so close() can't be starved by back-to-back polls @@ -88,22 +88,22 @@ public final class LumenConnection { pinSHA256: Data? = nil, timeoutMs: UInt32 = 10_000 ) throws { - if let pin = pinSHA256, pin.count != 32 { throw LumenClientError.invalidPin } + if let pin = pinSHA256, pin.count != 32 { throw PunktfunkClientError.invalidPin } var observed = [UInt8](repeating: 0, count: 32) handle = host.withCString { cs in if let pin = pinSHA256 { return pin.withUnsafeBytes { p in - lumen_connect( + punktfunk_connect( cs, port, width, height, refreshHz, p.bindMemory(to: UInt8.self).baseAddress, &observed, timeoutMs) } } - return lumen_connect(cs, port, width, height, refreshHz, nil, &observed, timeoutMs) + return punktfunk_connect(cs, port, width, height, refreshHz, nil, &observed, timeoutMs) } - guard handle != nil else { throw LumenClientError.connectFailed } + guard handle != nil else { throw PunktfunkClientError.connectFailed } hostFingerprint = Data(observed) var w: UInt32 = 0, h: UInt32 = 0, hz: UInt32 = 0 - _ = lumen_connection_mode(handle, &w, &h, &hz) + _ = punktfunk_connection_mode(handle, &w, &h, &hz) self.width = w self.height = h self.refreshHz = hz @@ -114,10 +114,10 @@ public final class LumenConnection { public func nextAU(timeoutMs: UInt32 = 100) throws -> AccessUnit? { pumpLock.lock() defer { pumpLock.unlock() } - guard let h = liveHandle() else { throw LumenClientError.closed } + guard let h = liveHandle() else { throw PunktfunkClientError.closed } - var frame = LumenFrame() - let rc = lumen_connection_next_au(h, &frame, timeoutMs) + var frame = PunktfunkFrame() + let rc = punktfunk_connection_next_au(h, &frame, timeoutMs) switch rc { case statusOK: guard let base = frame.data, frame.len > 0 else { return nil } @@ -128,9 +128,9 @@ public final class LumenConnection { case statusNoFrame: return nil case statusClosed: - throw LumenClientError.closed + throw PunktfunkClientError.closed default: - throw LumenClientError.status(rc) + throw PunktfunkClientError.status(rc) } } @@ -140,10 +140,10 @@ public final class LumenConnection { public func nextAudio(timeoutMs: UInt32 = 100) throws -> AudioPacket? { audioLock.lock() defer { audioLock.unlock() } - guard let h = liveHandle() else { throw LumenClientError.closed } + guard let h = liveHandle() else { throw PunktfunkClientError.closed } - var pkt = LumenAudioPacket() - let rc = lumen_connection_next_audio(h, &pkt, timeoutMs) + var pkt = PunktfunkAudioPacket() + let rc = punktfunk_connection_next_audio(h, &pkt, timeoutMs) switch rc { case statusOK: guard let base = pkt.data, pkt.len > 0 else { return nil } @@ -152,9 +152,9 @@ public final class LumenConnection { case statusNoFrame: return nil case statusClosed: - throw LumenClientError.closed + throw PunktfunkClientError.closed default: - throw LumenClientError.status(rc) + throw PunktfunkClientError.status(rc) } } @@ -164,30 +164,30 @@ public final class LumenConnection { public func nextRumble(timeoutMs: UInt32 = 0) throws -> (pad: UInt16, low: UInt16, high: UInt16)? { audioLock.lock() defer { audioLock.unlock() } - guard let h = liveHandle() else { throw LumenClientError.closed } + guard let h = liveHandle() else { throw PunktfunkClientError.closed } var pad: UInt16 = 0, low: UInt16 = 0, high: UInt16 = 0 - let rc = lumen_connection_next_rumble(h, &pad, &low, &high, timeoutMs) + let rc = punktfunk_connection_next_rumble(h, &pad, &low, &high, timeoutMs) switch rc { case statusOK: return (pad, low, high) case statusNoFrame: return nil case statusClosed: - throw LumenClientError.closed + throw PunktfunkClientError.closed default: - throw LumenClientError.status(rc) + throw PunktfunkClientError.status(rc) } } /// Send one input event (delivered to the host as a QUIC datagram). Thread-safe; /// silently dropped after close. - public func send(_ event: LumenInputEvent) { + public func send(_ event: PunktfunkInputEvent) { var ev = event abiLock.lock() defer { abiLock.unlock() } guard let h = handle, !closeRequested else { return } - _ = lumen_connection_send_input(h, &ev) + _ = punktfunk_connection_send_input(h, &ev) } /// Close the connection and free the handle. Safe from any thread, idempotent; waits @@ -205,7 +205,7 @@ public final class LumenConnection { audioLock.unlock() pumpLock.unlock() if let h { - lumen_connection_close(h) // joins the connection's internal Rust threads + punktfunk_connection_close(h) // joins the connection's internal Rust threads } } @@ -220,46 +220,46 @@ public final class LumenConnection { } // Convenience constructors for the wire input events (field semantics match -// lumen_core::input::InputEvent; see lumen_core.h). -public extension LumenInputEvent { +// punktfunk_core::input::InputEvent; see punktfunk_core.h). +public extension PunktfunkInputEvent { private static func make( _ kind: UInt32, code: UInt32, x: Int32, y: Int32, flags: UInt32 = 0 - ) -> LumenInputEvent { - LumenInputEvent(kind: UInt8(kind), _pad: (0, 0, 0), code: code, x: x, y: y, flags: flags) + ) -> PunktfunkInputEvent { + PunktfunkInputEvent(kind: UInt8(kind), _pad: (0, 0, 0), code: code, x: x, y: y, flags: flags) } - static func mouseMove(dx: Int32, dy: Int32) -> LumenInputEvent { - make(LUMEN_INPUT_KIND_MOUSE_MOVE.rawValue, code: 0, x: dx, y: dy) + static func mouseMove(dx: Int32, dy: Int32) -> PunktfunkInputEvent { + make(PUNKTFUNK_INPUT_KIND_MOUSE_MOVE.rawValue, code: 0, x: dx, y: dy) } /// GameStream button ids: 1=left 2=middle 3=right 4=X1 5=X2 (host maps to evdev BTN_*). - static func mouseButton(_ button: UInt32, down: Bool) -> LumenInputEvent { + static func mouseButton(_ button: UInt32, down: Bool) -> PunktfunkInputEvent { make( - (down ? LUMEN_INPUT_KIND_MOUSE_BUTTON_DOWN : LUMEN_INPUT_KIND_MOUSE_BUTTON_UP).rawValue, + (down ? PUNKTFUNK_INPUT_KIND_MOUSE_BUTTON_DOWN : PUNKTFUNK_INPUT_KIND_MOUSE_BUTTON_UP).rawValue, code: button, x: 0, y: 0) } /// `vk` is a Windows virtual-key code (the host's vk_to_evdev table consumes these). - static func key(_ vk: UInt32, down: Bool) -> LumenInputEvent { - make((down ? LUMEN_INPUT_KIND_KEY_DOWN : LUMEN_INPUT_KIND_KEY_UP).rawValue, code: vk, x: 0, y: 0) + static func key(_ vk: UInt32, down: Bool) -> PunktfunkInputEvent { + make((down ? PUNKTFUNK_INPUT_KIND_KEY_DOWN : PUNKTFUNK_INPUT_KIND_KEY_UP).rawValue, code: vk, x: 0, y: 0) } /// WHEEL_DELTA(120)-scaled; positive = up (vertical) / right (horizontal) — the /// convention Moonlight/SDL use; the host maps onto the ei/wl axes. - static func scroll(_ delta: Int32, horizontal: Bool = false) -> LumenInputEvent { - make(LUMEN_INPUT_KIND_MOUSE_SCROLL.rawValue, code: horizontal ? 1 : 0, x: delta, y: 0) + static func scroll(_ delta: Int32, horizontal: Bool = false) -> PunktfunkInputEvent { + make(PUNKTFUNK_INPUT_KIND_MOUSE_SCROLL.rawValue, code: horizontal ? 1 : 0, x: delta, y: 0) } - // Gamepad (wire contract in lumen_core::input::gamepad): one transition per event, + // Gamepad (wire contract in punktfunk_core::input::gamepad): one transition per event, // `pad` = controller index, accumulated host-side into a virtual Xbox 360 pad. /// `button` is a GameStream buttonFlags bit (A=0x1000 B=0x2000 X=0x4000 Y=0x8000, /// dpad=0x1/2/4/8, start=0x10 back=0x20 LS=0x40 RS=0x80 LB=0x100 RB=0x200 guide=0x400). - static func gamepadButton(_ button: UInt32, down: Bool, pad: UInt32 = 0) -> LumenInputEvent { + static func gamepadButton(_ button: UInt32, down: Bool, pad: UInt32 = 0) -> PunktfunkInputEvent { make( - LUMEN_INPUT_KIND_GAMEPAD_BUTTON.rawValue, + PUNKTFUNK_INPUT_KIND_GAMEPAD_BUTTON.rawValue, code: button, x: down ? 1 : 0, y: 0, flags: pad) } /// Axis ids: 0=LSX 1=LSY 2=RSX 3=RSY (−32768...32767, XInput convention: +y = UP — /// `GCControllerDirectionPad.yAxis` already matches, no flip), 4=LT 5=RT (0...255). - static func gamepadAxis(_ axis: UInt32, value: Int32, pad: UInt32 = 0) -> LumenInputEvent { - make(LUMEN_INPUT_KIND_GAMEPAD_AXIS.rawValue, code: axis, x: value, y: 0, flags: pad) + static func gamepadAxis(_ axis: UInt32, value: Int32, pad: UInt32 = 0) -> PunktfunkInputEvent { + make(PUNKTFUNK_INPUT_KIND_GAMEPAD_AXIS.rawValue, code: axis, x: value, y: 0, flags: pad) } } diff --git a/clients/apple/Sources/LumenKit/StreamView.swift b/clients/apple/Sources/PunktfunkKit/StreamView.swift similarity index 91% rename from clients/apple/Sources/LumenKit/StreamView.swift rename to clients/apple/Sources/PunktfunkKit/StreamView.swift index 40bb87f1..c112e43b 100644 --- a/clients/apple/Sources/LumenKit/StreamView.swift +++ b/clients/apple/Sources/PunktfunkKit/StreamView.swift @@ -1,4 +1,4 @@ -// SwiftUI presentation: AVSampleBufferDisplayLayer fed straight from the lumen/1 connection. +// SwiftUI presentation: AVSampleBufferDisplayLayer fed straight from the punktfunk/1 connection. // // Stage-1 presenter (see README): the layer accepts *compressed* HEVC sample buffers and // does hardware decode + display itself — fastest path to pixels, IOSurface-backed @@ -13,13 +13,13 @@ import AVFoundation import SwiftUI public struct StreamView: NSViewRepresentable { - private let connection: LumenConnection + private let connection: PunktfunkConnection private let onFrame: (@Sendable (AccessUnit) -> Void)? private let onSessionEnd: (@Sendable () -> Void)? /// `onFrame`/`onSessionEnd` fire on the pump thread — hop to the main actor for UI. public init( - connection: LumenConnection, + connection: PunktfunkConnection, onFrame: (@Sendable (AccessUnit) -> Void)? = nil, onSessionEnd: (@Sendable () -> Void)? = nil ) { @@ -67,7 +67,7 @@ public final class StreamLayerView: NSView { private let displayLayer = AVSampleBufferDisplayLayer() private var token: PumpToken? - public private(set) var connection: LumenConnection? + public private(set) var connection: PunktfunkConnection? public override init(frame: NSRect) { super.init(frame: frame) @@ -81,7 +81,7 @@ public final class StreamLayerView: NSView { /// Pump thread: pull AUs from the connection, wrap, enqueue. The first IDR yields the /// format description; non-IDR AUs before it are dropped (the host opens with an IDR). public func start( - connection: LumenConnection, + connection: PunktfunkConnection, onFrame: (@Sendable (AccessUnit) -> Void)? = nil, onSessionEnd: (@Sendable () -> Void)? = nil ) { @@ -104,7 +104,7 @@ public final class StreamLayerView: NSView { if layer.status == .failed { // Decode wedged: flush and re-gate on the next in-band parameter // sets — resuming with a delta frame can't recover. (A - // request-IDR channel on lumen/1 is a host-side TODO; with the + // request-IDR channel on punktfunk/1 is a host-side TODO; with the // host's infinite GOP this may otherwise stay black until the // next recovery keyframe.) layer.flush() @@ -123,13 +123,13 @@ public final class StreamLayerView: NSView { } } } - thread.name = "lumen-pump" + thread.name = "punktfunk-pump" thread.qualityOfService = .userInteractive thread.start() } /// Stop pumping (≤ one poll timeout). Does not close the connection — that stays with - /// whoever owns it (LumenConnection.close() is safe alongside a draining pump). + /// whoever owns it (PunktfunkConnection.close() is safe alongside a draining pump). public func stop() { token?.cancel() token = nil diff --git a/clients/apple/Tests/LumenKitTests/AnnexBTests.swift b/clients/apple/Tests/PunktfunkKitTests/AnnexBTests.swift similarity index 98% rename from clients/apple/Tests/LumenKitTests/AnnexBTests.swift rename to clients/apple/Tests/PunktfunkKitTests/AnnexBTests.swift index e4a32edf..8bde7056 100644 --- a/clients/apple/Tests/LumenKitTests/AnnexBTests.swift +++ b/clients/apple/Tests/PunktfunkKitTests/AnnexBTests.swift @@ -2,7 +2,7 @@ // VideoToolboxRoundTripTests covers the real-bitstream path). import XCTest -@testable import LumenKit +@testable import PunktfunkKit final class AnnexBTests: XCTestCase { /// NAL with the given HEVC type in bits 1..6 of the first header byte. diff --git a/clients/apple/Tests/LumenKitTests/LoopbackIntegrationTests.swift b/clients/apple/Tests/PunktfunkKitTests/LoopbackIntegrationTests.swift similarity index 82% rename from clients/apple/Tests/LumenKitTests/LoopbackIntegrationTests.swift rename to clients/apple/Tests/PunktfunkKitTests/LoopbackIntegrationTests.swift index d7a0b137..21f4baa3 100644 --- a/clients/apple/Tests/LumenKitTests/LoopbackIntegrationTests.swift +++ b/clients/apple/Tests/PunktfunkKitTests/LoopbackIntegrationTests.swift @@ -1,20 +1,20 @@ -// Integration: the Swift wrapper against a real lumen/1 host over QUIC + UDP on loopback — -// the Swift twin of lumen-host's m3.rs::c_abi_connection_roundtrip, this time through the +// Integration: the Swift wrapper against a real punktfunk/1 host over QUIC + UDP on loopback — +// the Swift twin of punktfunk-host's m3.rs::c_abi_connection_roundtrip, this time through the // statically linked xcframework. Driven by clients/apple/test-loopback.sh, which builds and -// starts `lumen-host m3-host --source synthetic` and sets LUMEN_LOOPBACK_PORT. +// starts `punktfunk-host m3-host --source synthetic` and sets PUNKTFUNK_LOOPBACK_PORT. import XCTest -@testable import LumenKit +@testable import PunktfunkKit final class LoopbackIntegrationTests: XCTestCase { func testSyntheticStreamRoundTrip() throws { - guard let portStr = ProcessInfo.processInfo.environment["LUMEN_LOOPBACK_PORT"], + guard let portStr = ProcessInfo.processInfo.environment["PUNKTFUNK_LOOPBACK_PORT"], let port = UInt16(portStr) else { throw XCTSkip("needs a running m3-host — use clients/apple/test-loopback.sh") } - let conn = try LumenConnection( + let conn = try PunktfunkConnection( host: "127.0.0.1", port: port, width: 1280, height: 720, refreshHz: 60) XCTAssertEqual(conn.width, 1280) XCTAssertEqual(conn.height, 720) @@ -49,7 +49,7 @@ final class LoopbackIntegrationTests: XCTestCase { conn.close() XCTAssertThrowsError(try conn.nextAU(timeoutMs: 10)) { error in - guard case LumenClientError.closed = error else { + guard case PunktfunkClientError.closed = error else { return XCTFail("expected .closed, got \(error)") } } @@ -58,7 +58,7 @@ final class LoopbackIntegrationTests: XCTestCase { func testConnectFailureThrows() { // Nothing listens on this port; connect must fail within its timeout, not hang. XCTAssertThrowsError( - try LumenConnection( + try PunktfunkConnection( host: "127.0.0.1", port: 9, width: 640, height: 480, refreshHz: 30, timeoutMs: 2000)) } diff --git a/clients/apple/Tests/LumenKitTests/RemoteFirstLightTests.swift b/clients/apple/Tests/PunktfunkKitTests/RemoteFirstLightTests.swift similarity index 85% rename from clients/apple/Tests/LumenKitTests/RemoteFirstLightTests.swift rename to clients/apple/Tests/PunktfunkKitTests/RemoteFirstLightTests.swift index afd094ef..aba9b2b9 100644 --- a/clients/apple/Tests/LumenKitTests/RemoteFirstLightTests.swift +++ b/clients/apple/Tests/PunktfunkKitTests/RemoteFirstLightTests.swift @@ -4,25 +4,25 @@ // putting the layer on glass. // // Run (host side, on the Linux box): -// LUMEN_COMPOSITOR=gamescope LUMEN_GAMESCOPE_APP=vkcube LUMEN_ZEROCOPY=1 \ -// lumen-host m3-host --source virtual --seconds 120 +// PUNKTFUNK_COMPOSITOR=gamescope PUNKTFUNK_GAMESCOPE_APP=vkcube PUNKTFUNK_ZEROCOPY=1 \ +// punktfunk-host m3-host --source virtual --seconds 120 // Then here: -// LUMEN_REMOTE_HOST=192.168.1.70 swift test --filter RemoteFirstLightTests +// PUNKTFUNK_REMOTE_HOST=192.168.1.70 swift test --filter RemoteFirstLightTests import CoreMedia import VideoToolbox import XCTest -@testable import LumenKit +@testable import PunktfunkKit final class RemoteFirstLightTests: XCTestCase { func testRemoteStreamDecodesToPixels() throws { - guard let host = ProcessInfo.processInfo.environment["LUMEN_REMOTE_HOST"] else { - throw XCTSkip("set LUMEN_REMOTE_HOST (and start m3-host --source virtual there)") + guard let host = ProcessInfo.processInfo.environment["PUNKTFUNK_REMOTE_HOST"] else { + throw XCTSkip("set PUNKTFUNK_REMOTE_HOST (and start m3-host --source virtual there)") } let width: UInt32 = 1280 let height: UInt32 = 720 - let conn = try LumenConnection( + let conn = try PunktfunkConnection( host: host, width: width, height: height, refreshHz: 60) defer { conn.close() } XCTAssertEqual(conn.width, width) diff --git a/clients/apple/Tests/LumenKitTests/VideoToolboxRoundTripTests.swift b/clients/apple/Tests/PunktfunkKitTests/VideoToolboxRoundTripTests.swift similarity index 98% rename from clients/apple/Tests/LumenKitTests/VideoToolboxRoundTripTests.swift rename to clients/apple/Tests/PunktfunkKitTests/VideoToolboxRoundTripTests.swift index e9ab4299..03c3b46b 100644 --- a/clients/apple/Tests/LumenKitTests/VideoToolboxRoundTripTests.swift +++ b/clients/apple/Tests/PunktfunkKitTests/VideoToolboxRoundTripTests.swift @@ -1,13 +1,13 @@ // Real-bitstream proof of the decode-prep path: VTCompressionSession encodes HEVC, we // rebuild the host's wire shape (Annex-B AU with in-band VPS/SPS/PPS — exactly what -// lumen-host emits on every IDR), run it through AnnexB, and hand the result to a real +// punktfunk-host emits on every IDR), run it through AnnexB, and hand the result to a real // VTDecompressionSession. Pixels out = the whole client decode path is sound. import AVFoundation import CoreMedia import VideoToolbox import XCTest -@testable import LumenKit +@testable import PunktfunkKit final class VideoToolboxRoundTripTests: XCTestCase { private let width = 320 diff --git a/clients/apple/test-loopback.sh b/clients/apple/test-loopback.sh index 3c218962..16a0ec0a 100755 --- a/clients/apple/test-loopback.sh +++ b/clients/apple/test-loopback.sh @@ -1,17 +1,17 @@ #!/usr/bin/env bash -# Loopback integration: a real lumen/1 host (synthetic source — pure protocol, runs fine on +# Loopback integration: a real punktfunk/1 host (synthetic source — pure protocol, runs fine on # macOS) on 127.0.0.1, then the Swift integration tests against it through the xcframework. # The m3 host serves exactly one session and exits; the trap is just for failure paths. set -euo pipefail cd "$(dirname "$0")/../.." -PORT="${LUMEN_LOOPBACK_PORT:-19778}" +PORT="${PUNKTFUNK_LOOPBACK_PORT:-19778}" -cargo build --release -p lumen-host -target/release/lumen-host m3-host --port "$PORT" --source synthetic --frames 300 & +cargo build --release -p punktfunk-host +target/release/punktfunk-host m3-host --port "$PORT" --source synthetic --frames 300 & HOST_PID=$! trap 'kill "$HOST_PID" 2>/dev/null || true' EXIT sleep 1 cd clients/apple -LUMEN_LOOPBACK_PORT="$PORT" swift test --filter LoopbackIntegrationTests +PUNKTFUNK_LOOPBACK_PORT="$PORT" swift test --filter LoopbackIntegrationTests diff --git a/crates/lumen-core/cbindgen.toml b/crates/lumen-core/cbindgen.toml deleted file mode 100644 index ea528963..00000000 --- a/crates/lumen-core/cbindgen.toml +++ /dev/null @@ -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 (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" diff --git a/crates/lumen-client-rs/Cargo.toml b/crates/punktfunk-client-rs/Cargo.toml similarity index 67% rename from crates/lumen-client-rs/Cargo.toml rename to crates/punktfunk-client-rs/Cargo.toml index c782de15..a7f80534 100644 --- a/crates/lumen-client-rs/Cargo.toml +++ b/crates/punktfunk-client-rs/Cargo.toml @@ -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" diff --git a/crates/lumen-client-rs/src/main.rs b/crates/punktfunk-client-rs/src/main.rs similarity index 91% rename from crates/lumen-client-rs/src/main.rs rename to crates/punktfunk-client-rs/src/main.rs index 90ea18f2..856ad209 100644 --- a/crates/lumen-client-rs/src/main.rs +++ b/crates/punktfunk-client-rs/src/main.rs @@ -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 { let mut d = vec![0u8; len]; if len >= 4 { diff --git a/crates/lumen-core/Cargo.toml b/crates/punktfunk-core/Cargo.toml similarity index 81% rename from crates/lumen-core/Cargo.toml rename to crates/punktfunk-core/Cargo.toml index dc2bb39d..023228bb 100644 --- a/crates/lumen-core/Cargo.toml +++ b/crates/punktfunk-core/Cargo.toml @@ -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] diff --git a/crates/lumen-core/build.rs b/crates/punktfunk-core/build.rs similarity index 70% rename from crates/lumen-core/build.rs rename to crates/punktfunk-core/build.rs index f0a8551f..134c8c8f 100644 --- a/crates/lumen-core/build.rs +++ b/crates/punktfunk-core/build.rs @@ -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"); } } } diff --git a/crates/punktfunk-core/cbindgen.toml b/crates/punktfunk-core/cbindgen.toml new file mode 100644 index 00000000..c3166667 --- /dev/null +++ b/crates/punktfunk-core/cbindgen.toml @@ -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 (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" diff --git a/crates/lumen-core/src/abi.rs b/crates/punktfunk-core/src/abi.rs similarity index 71% rename from crates/lumen-core/src/abi.rs rename to crates/punktfunk-core/src/abi.rs index 94fe5556..de389491 100644 --- a/crates/lumen-core/src/abi.rs +++ b/crates/punktfunk-core/src/abi.rs @@ -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, - 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 { +impl PunktfunkConfig { + fn to_config(self) -> Result { 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 { +unsafe fn config_from_ptr(cfg: *const PunktfunkConfig) -> Result { 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::() { - return Err(LumenStatus::InvalidArg); + if declared < std::mem::size_of::() { + 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 for LumenStats { +impl From 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 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 LumenStatus>(f: F) -> LumenStatus { - std::panic::catch_unwind(AssertUnwindSafe(f)).unwrap_or(LumenStatus::Panic) +fn guard 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` (not the `LumenInputCb` alias) so cbindgen +pub unsafe extern "C" fn punktfunk_set_input_callback( + s: *mut PunktfunkSession, + // Written as an explicit `Option` (not the `PunktfunkInputCb` alias) so cbindgen // emits a nullable C function pointer rather than an opaque wrapper struct. cb: Option, 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>, - /// 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>, } -/// 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) }); } diff --git a/crates/lumen-core/src/client.rs b/crates/punktfunk-core/src/client.rs similarity index 84% rename from crates/lumen-core/src/client.rs rename to crates/punktfunk-core/src/client.rs index 39979867..2c329e20 100644 --- a/crates/lumen-core/src/client.rs +++ b/crates/punktfunk-core/src/client.rs @@ -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 { 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 { 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, diff --git a/crates/lumen-core/src/config.rs b/crates/punktfunk-core/src/config.rs similarity index 92% rename from crates/lumen-core/src/config.rs rename to crates/punktfunk-core/src/config.rs index ed303a53..feae7ef9 100644 --- a/crates/lumen-core/src/config.rs +++ b/crates/punktfunk-core/src/config.rs @@ -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)", )); } diff --git a/crates/lumen-core/src/crypto.rs b/crates/punktfunk-core/src/crypto.rs similarity index 97% rename from crates/lumen-core/src/crypto.rs rename to crates/punktfunk-core/src/crypto.rs index 2917096d..2ec5d637 100644 --- a/crates/lumen-core/src/crypto.rs +++ b/crates/punktfunk-core/src/crypto.rs @@ -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) } } diff --git a/crates/lumen-core/src/error.rs b/crates/punktfunk-core/src/error.rs similarity index 56% rename from crates/lumen-core/src/error.rs rename to crates/punktfunk-core/src/error.rs index f383c108..e299e7dc 100644 --- a/crates/lumen-core/src/error.rs +++ b/crates/punktfunk-core/src/error.rs @@ -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 = core::result::Result; +pub type Result = core::result::Result; /// 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, } } } diff --git a/crates/lumen-core/src/fec/gf16.rs b/crates/punktfunk-core/src/fec/gf16.rs similarity index 100% rename from crates/lumen-core/src/fec/gf16.rs rename to crates/punktfunk-core/src/fec/gf16.rs diff --git a/crates/lumen-core/src/fec/gf8.rs b/crates/punktfunk-core/src/fec/gf8.rs similarity index 100% rename from crates/lumen-core/src/fec/gf8.rs rename to crates/punktfunk-core/src/fec/gf8.rs diff --git a/crates/lumen-core/src/fec/mod.rs b/crates/punktfunk-core/src/fec/mod.rs similarity index 100% rename from crates/lumen-core/src/fec/mod.rs rename to crates/punktfunk-core/src/fec/mod.rs diff --git a/crates/lumen-core/src/input.rs b/crates/punktfunk-core/src/input.rs similarity index 99% rename from crates/lumen-core/src/input.rs rename to crates/punktfunk-core/src/input.rs index d1e7434f..84a38ca2 100644 --- a/crates/lumen-core/src/input.rs +++ b/crates/punktfunk-core/src/input.rs @@ -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 { diff --git a/crates/lumen-core/src/lib.rs b/crates/punktfunk-core/src/lib.rs similarity index 87% rename from crates/lumen-core/src/lib.rs rename to crates/punktfunk-core/src/lib.rs index 2ba9240d..090b3519 100644 --- a/crates/lumen-core/src/lib.rs +++ b/crates/punktfunk-core/src/lib.rs @@ -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; diff --git a/crates/lumen-core/src/packet.rs b/crates/punktfunk-core/src/packet.rs similarity index 97% rename from crates/lumen-core/src/packet.rs rename to crates/punktfunk-core/src/packet.rs index 0bad48aa..8d5b5b83 100644 --- a/crates/lumen-core/src/packet.rs +++ b/crates/punktfunk-core/src/packet.rs @@ -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, diff --git a/crates/lumen-core/src/quic.rs b/crates/punktfunk-core/src/quic.rs similarity index 96% rename from crates/lumen-core/src/quic.rs rename to crates/punktfunk-core/src/quic.rs index e3df3678..0c90f05a 100644 --- a/crates/lumen-core/src/quic.rs +++ b/crates/punktfunk-core/src/quic.rs @@ -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 { 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 { 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 { - 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 = std::result::Result; #[derive(Debug)] diff --git a/crates/lumen-core/src/session.rs b/crates/punktfunk-core/src/session.rs similarity index 93% rename from crates/lumen-core/src/session.rs rename to crates/punktfunk-core/src/session.rs index ab4bb978..e26a8d0d 100644 --- a/crates/lumen-core/src/session.rs +++ b/crates/punktfunk-core/src/session.rs @@ -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> { 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 { 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", )); } diff --git a/crates/lumen-core/src/stats.rs b/crates/punktfunk-core/src/stats.rs similarity index 96% rename from crates/lumen-core/src/stats.rs rename to crates/punktfunk-core/src/stats.rs index 5b6974bb..74a798cd 100644 --- a/crates/lumen-core/src/stats.rs +++ b/crates/punktfunk-core/src/stats.rs @@ -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, diff --git a/crates/lumen-core/src/transport/loopback.rs b/crates/punktfunk-core/src/transport/loopback.rs similarity index 100% rename from crates/lumen-core/src/transport/loopback.rs rename to crates/punktfunk-core/src/transport/loopback.rs diff --git a/crates/lumen-core/src/transport/mod.rs b/crates/punktfunk-core/src/transport/mod.rs similarity index 100% rename from crates/lumen-core/src/transport/mod.rs rename to crates/punktfunk-core/src/transport/mod.rs diff --git a/crates/lumen-core/src/transport/udp.rs b/crates/punktfunk-core/src/transport/udp.rs similarity index 100% rename from crates/lumen-core/src/transport/udp.rs rename to crates/punktfunk-core/src/transport/udp.rs diff --git a/crates/lumen-core/tests/c/harness.c b/crates/punktfunk-core/tests/c/harness.c similarity index 73% rename from crates/lumen-core/tests/c/harness.c rename to crates/punktfunk-core/tests/c/harness.c index c6e8c24b..37e0476b 100644 --- a/crates/lumen-core/tests/c/harness.c +++ b/crates/punktfunk-core/tests/c/harness.c @@ -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 #include #include -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); diff --git a/crates/lumen-core/tests/c/run.sh b/crates/punktfunk-core/tests/c/run.sh similarity index 57% rename from crates/lumen-core/tests/c/run.sh rename to crates/punktfunk-core/tests/c/run.sh index 1bfb0e7a..dd0b368e 100755 --- a/crates/lumen-core/tests/c/run.sh +++ b/crates/punktfunk-core/tests/c/run.sh @@ -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:-}" -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" \ diff --git a/crates/lumen-core/tests/c_abi.rs b/crates/punktfunk-core/tests/c_abi.rs similarity index 84% rename from crates/lumen-core/tests/c_abi.rs rename to crates/punktfunk-core/tests/c_abi.rs index da3a06cb..b35f353d 100644 --- a/crates/lumen-core/tests/c_abi.rs +++ b/crates/punktfunk-core/tests/c_abi.rs @@ -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 diff --git a/crates/lumen-core/tests/loopback.rs b/crates/punktfunk-core/tests/loopback.rs similarity index 94% rename from crates/lumen-core/tests/loopback.rs rename to crates/punktfunk-core/tests/loopback.rs index f7a958ff..d4aeb10b 100644 --- a/crates/lumen-core/tests/loopback.rs +++ b/crates/punktfunk-core/tests/loopback.rs @@ -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], -) -> 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), diff --git a/crates/lumen-core/vendor/fec-rs/Cargo.toml b/crates/punktfunk-core/vendor/fec-rs/Cargo.toml similarity index 100% rename from crates/lumen-core/vendor/fec-rs/Cargo.toml rename to crates/punktfunk-core/vendor/fec-rs/Cargo.toml diff --git a/crates/lumen-core/vendor/fec-rs/LICENSE b/crates/punktfunk-core/vendor/fec-rs/LICENSE similarity index 100% rename from crates/lumen-core/vendor/fec-rs/LICENSE rename to crates/punktfunk-core/vendor/fec-rs/LICENSE diff --git a/crates/lumen-core/vendor/fec-rs/README.md b/crates/punktfunk-core/vendor/fec-rs/README.md similarity index 100% rename from crates/lumen-core/vendor/fec-rs/README.md rename to crates/punktfunk-core/vendor/fec-rs/README.md diff --git a/crates/lumen-core/vendor/fec-rs/build.rs b/crates/punktfunk-core/vendor/fec-rs/build.rs similarity index 100% rename from crates/lumen-core/vendor/fec-rs/build.rs rename to crates/punktfunk-core/vendor/fec-rs/build.rs diff --git a/crates/lumen-core/vendor/fec-rs/src/errors.rs b/crates/punktfunk-core/vendor/fec-rs/src/errors.rs similarity index 100% rename from crates/lumen-core/vendor/fec-rs/src/errors.rs rename to crates/punktfunk-core/vendor/fec-rs/src/errors.rs diff --git a/crates/lumen-core/vendor/fec-rs/src/galois.rs b/crates/punktfunk-core/vendor/fec-rs/src/galois.rs similarity index 100% rename from crates/lumen-core/vendor/fec-rs/src/galois.rs rename to crates/punktfunk-core/vendor/fec-rs/src/galois.rs diff --git a/crates/lumen-core/vendor/fec-rs/src/lib.rs b/crates/punktfunk-core/vendor/fec-rs/src/lib.rs similarity index 100% rename from crates/lumen-core/vendor/fec-rs/src/lib.rs rename to crates/punktfunk-core/vendor/fec-rs/src/lib.rs diff --git a/crates/lumen-core/vendor/fec-rs/src/matrix.rs b/crates/punktfunk-core/vendor/fec-rs/src/matrix.rs similarity index 100% rename from crates/lumen-core/vendor/fec-rs/src/matrix.rs rename to crates/punktfunk-core/vendor/fec-rs/src/matrix.rs diff --git a/crates/lumen-core/vendor/fec-rs/src/reed_solomon.rs b/crates/punktfunk-core/vendor/fec-rs/src/reed_solomon.rs similarity index 100% rename from crates/lumen-core/vendor/fec-rs/src/reed_solomon.rs rename to crates/punktfunk-core/vendor/fec-rs/src/reed_solomon.rs diff --git a/crates/lumen-host/Cargo.toml b/crates/punktfunk-host/Cargo.toml similarity index 93% rename from crates/lumen-host/Cargo.toml rename to crates/punktfunk-host/Cargo.toml index 86074138..84bb4857 100644 --- a/crates/lumen-host/Cargo.toml +++ b/crates/punktfunk-host/Cargo.toml @@ -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" diff --git a/crates/lumen-host/protocols/zkde-screencast-unstable-v1.xml b/crates/punktfunk-host/protocols/zkde-screencast-unstable-v1.xml similarity index 100% rename from crates/lumen-host/protocols/zkde-screencast-unstable-v1.xml rename to crates/punktfunk-host/protocols/zkde-screencast-unstable-v1.xml diff --git a/crates/lumen-host/src/audio.rs b/crates/punktfunk-host/src/audio.rs similarity index 100% rename from crates/lumen-host/src/audio.rs rename to crates/punktfunk-host/src/audio.rs diff --git a/crates/lumen-host/src/audio/linux.rs b/crates/punktfunk-host/src/audio/linux.rs similarity index 98% rename from crates/lumen-host/src/audio/linux.rs rename to crates/punktfunk-host/src/audio/linux.rs index 6de725fb..e908e744 100644 --- a/crates/lumen-host/src/audio/linux.rs +++ b/crates/punktfunk-host/src/audio/linux.rs @@ -20,7 +20,7 @@ impl PwAudioCapturer { pub fn open() -> Result { let (tx, rx) = sync_channel::>(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>) -> Result<()> { let stream = pw::stream::StreamBox::new( &core, - "lumen-audio", + "punktfunk-audio", properties! { *pw::keys::MEDIA_TYPE => "Audio", *pw::keys::MEDIA_CATEGORY => "Capture", diff --git a/crates/lumen-host/src/capture.rs b/crates/punktfunk-host/src/capture.rs similarity index 98% rename from crates/lumen-host/src/capture.rs rename to crates/punktfunk-host/src/capture.rs index 185321d4..ed07a580 100644 --- a/crates/lumen-host/src/capture.rs +++ b/crates/punktfunk-host/src/capture.rs @@ -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, diff --git a/crates/lumen-host/src/capture/linux.rs b/crates/punktfunk-host/src/capture/linux.rs similarity index 99% rename from crates/lumen-host/src/capture/linux.rs rename to crates/punktfunk-host/src/capture/linux.rs index 07c4c4a5..9c2ae75f 100644 --- a/crates/lumen-host/src/capture/linux.rs +++ b/crates/punktfunk-host/src/capture/linux.rs @@ -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::>(); 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()?))); diff --git a/crates/lumen-host/src/encode.rs b/crates/punktfunk-host/src/encode.rs similarity index 98% rename from crates/lumen-host/src/encode.rs rename to crates/punktfunk-host/src/encode.rs index af3c2e83..8b1015cc 100644 --- a/crates/lumen-host/src/encode.rs +++ b/crates/punktfunk-host/src/encode.rs @@ -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. diff --git a/crates/lumen-host/src/encode/linux.rs b/crates/punktfunk-host/src/encode/linux.rs similarity index 98% rename from crates/lumen-host/src/encode/linux.rs rename to crates/punktfunk-host/src/encode/linux.rs index 564d55f1..384be786 100644 --- a/crates/lumen-host/src/encode/linux.rs +++ b/crates/punktfunk-host/src/encode/linux.rs @@ -139,7 +139,7 @@ impl NvencEncoder { cuda: bool, ) -> Result { 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 => { diff --git a/crates/lumen-host/src/gamestream/apps.rs b/crates/punktfunk-host/src/gamestream/apps.rs similarity index 97% rename from crates/lumen-host/src/gamestream/apps.rs rename to crates/punktfunk-host/src/gamestream/apps.rs index 1755bdad..7cc6f276 100644 --- a/crates/lumen-host/src/gamestream/apps.rs +++ b/crates/punktfunk-host/src/gamestream/apps.rs @@ -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 { - 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 { diff --git a/crates/lumen-host/src/gamestream/audio.rs b/crates/punktfunk-host/src/gamestream/audio.rs similarity index 97% rename from crates/lumen-host/src/gamestream/audio.rs rename to crates/punktfunk-host/src/gamestream/audio.rs index 27fac9c8..b4085620 100644 --- a/crates/lumen-host/src/gamestream/audio.rs +++ b/crates/punktfunk-host/src/gamestream/audio.rs @@ -38,7 +38,7 @@ pub type AudioCapSlot = Arc>>>; /// `gcm_key`/`rikeyid` come from `/launch` and key the AES-CBC payload encryption. pub fn start(running: Arc, 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); diff --git a/crates/lumen-host/src/gamestream/cert.rs b/crates/punktfunk-host/src/gamestream/cert.rs similarity index 97% rename from crates/lumen-host/src/gamestream/cert.rs rename to crates/punktfunk-host/src/gamestream/cert.rs index 7ade3411..94d5a3d8 100644 --- a/crates/lumen-host/src/gamestream/cert.rs +++ b/crates/punktfunk-host/src/gamestream/cert.rs @@ -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::::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")?; diff --git a/crates/lumen-host/src/gamestream/control.rs b/crates/punktfunk-host/src/gamestream/control.rs similarity index 99% rename from crates/lumen-host/src/gamestream/control.rs rename to crates/punktfunk-host/src/gamestream/control.rs index 489e157c..7214142e 100644 --- a/crates/lumen-host/src/gamestream/control.rs +++ b/crates/punktfunk-host/src/gamestream/control.rs @@ -51,7 +51,7 @@ pub fn spawn(state: Arc) -> 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) { diff --git a/crates/lumen-host/src/gamestream/crypto.rs b/crates/punktfunk-host/src/gamestream/crypto.rs similarity index 98% rename from crates/lumen-host/src/gamestream/crypto.rs rename to crates/punktfunk-host/src/gamestream/crypto.rs index 23791f5c..6f91c014 100644 --- a/crates/lumen-host/src/gamestream/crypto.rs +++ b/crates/punktfunk-host/src/gamestream/crypto.rs @@ -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`. diff --git a/crates/lumen-host/src/gamestream/gamepad.rs b/crates/punktfunk-host/src/gamestream/gamepad.rs similarity index 100% rename from crates/lumen-host/src/gamestream/gamepad.rs rename to crates/punktfunk-host/src/gamestream/gamepad.rs diff --git a/crates/lumen-host/src/gamestream/input.rs b/crates/punktfunk-host/src/gamestream/input.rs similarity index 98% rename from crates/lumen-host/src/gamestream/input.rs rename to crates/punktfunk-host/src/gamestream/input.rs index 83470c01..0e554cd9 100644 --- a/crates/lumen-host/src/gamestream/input.rs +++ b/crates/punktfunk-host/src/gamestream/input.rs @@ -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; diff --git a/crates/lumen-host/src/gamestream/mdns.rs b/crates/punktfunk-host/src/gamestream/mdns.rs similarity index 100% rename from crates/lumen-host/src/gamestream/mdns.rs rename to crates/punktfunk-host/src/gamestream/mdns.rs diff --git a/crates/lumen-host/src/gamestream/mod.rs b/crates/punktfunk-host/src/gamestream/mod.rs similarity index 96% rename from crates/lumen-host/src/gamestream/mod.rs rename to crates/punktfunk-host/src/gamestream/mod.rs index b630bdfe..3094b59b 100644 --- a/crates/lumen-host/src/gamestream/mod.rs +++ b/crates/punktfunk-host/src/gamestream/mod.rs @@ -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 { /// Where the paired-client allow-list persists (survives host restarts, like Sunshine). fn paired_path() -> Option { - 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). diff --git a/crates/lumen-host/src/gamestream/nvhttp.rs b/crates/punktfunk-host/src/gamestream/nvhttp.rs similarity index 99% rename from crates/lumen-host/src/gamestream/nvhttp.rs rename to crates/punktfunk-host/src/gamestream/nvhttp.rs index 72b39790..f1729986 100644 --- a/crates/lumen-host/src/gamestream/nvhttp.rs +++ b/crates/punktfunk-host/src/gamestream/nvhttp.rs @@ -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. diff --git a/crates/lumen-host/src/gamestream/pairing.rs b/crates/punktfunk-host/src/gamestream/pairing.rs similarity index 100% rename from crates/lumen-host/src/gamestream/pairing.rs rename to crates/punktfunk-host/src/gamestream/pairing.rs diff --git a/crates/lumen-host/src/gamestream/rtsp.rs b/crates/punktfunk-host/src/gamestream/rtsp.rs similarity index 99% rename from crates/lumen-host/src/gamestream/rtsp.rs rename to crates/punktfunk-host/src/gamestream/rtsp.rs index 65f2e467..8808c4f1 100644 --- a/crates/lumen-host/src/gamestream/rtsp.rs +++ b/crates/punktfunk-host/src/gamestream/rtsp.rs @@ -27,7 +27,7 @@ pub fn spawn(state: Arc) -> 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 { diff --git a/crates/lumen-host/src/gamestream/serverinfo.rs b/crates/punktfunk-host/src/gamestream/serverinfo.rs similarity index 100% rename from crates/lumen-host/src/gamestream/serverinfo.rs rename to crates/punktfunk-host/src/gamestream/serverinfo.rs diff --git a/crates/lumen-host/src/gamestream/stream.rs b/crates/punktfunk-host/src/gamestream/stream.rs similarity index 96% rename from crates/lumen-host/src/gamestream/stream.rs rename to crates/punktfunk-host/src/gamestream/stream.rs index 239f0225..9b039b95 100644 --- a/crates/lumen-host/src/gamestream/stream.rs +++ b/crates/punktfunk-host/src/gamestream/stream.rs @@ -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. diff --git a/crates/lumen-host/src/gamestream/tls.rs b/crates/punktfunk-host/src/gamestream/tls.rs similarity index 100% rename from crates/lumen-host/src/gamestream/tls.rs rename to crates/punktfunk-host/src/gamestream/tls.rs diff --git a/crates/lumen-host/src/gamestream/video.rs b/crates/punktfunk-host/src/gamestream/video.rs similarity index 98% rename from crates/lumen-host/src/gamestream/video.rs rename to crates/punktfunk-host/src/gamestream/video.rs index a1d4488d..70ce517d 100644 --- a/crates/lumen-host/src/gamestream/video.rs +++ b/crates/punktfunk-host/src/gamestream/video.rs @@ -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; diff --git a/crates/lumen-host/src/inject.rs b/crates/punktfunk-host/src/inject.rs similarity index 95% rename from crates/lumen-host/src/inject.rs rename to crates/punktfunk-host/src/inject.rs index ef0d6857..0ed05769 100644 --- a/crates/lumen-host/src/inject.rs +++ b/crates/punktfunk-host/src/inject.rs @@ -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> { /// 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(); diff --git a/crates/lumen-host/src/inject/gamepad.rs b/crates/punktfunk-host/src/inject/gamepad.rs similarity index 98% rename from crates/lumen-host/src/inject/gamepad.rs rename to crates/punktfunk-host/src/inject/gamepad.rs index f23305f0..49f38cb6 100644 --- a/crates/lumen-host/src/inject/gamepad.rs +++ b/crates/punktfunk-host/src/inject/gamepad.rs @@ -13,7 +13,7 @@ //! //! All ioctl numbers/struct layouts below were verified against this generation's //! `` 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() ); } diff --git a/crates/lumen-host/src/inject/libei.rs b/crates/punktfunk-host/src/inject/libei.rs similarity index 98% rename from crates/lumen-host/src/inject/libei.rs rename to crates/punktfunk-host/src/inject/libei.rs index fd315ab6..0ca5b90d 100644 --- a/crates/lumen-host/src/inject/libei.rs +++ b/crates/punktfunk-host/src/inject/libei.rs @@ -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 { let (tx, rx) = unbounded_channel::(); 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 { }; 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)) diff --git a/crates/lumen-host/src/inject/wlr.rs b/crates/punktfunk-host/src/inject/wlr.rs similarity index 99% rename from crates/lumen-host/src/inject/wlr.rs rename to crates/punktfunk-host/src/inject/wlr.rs index 96c36668..e28c5961 100644 --- a/crates/lumen-host/src/inject/wlr.rs +++ b/crates/punktfunk-host/src/inject/wlr.rs @@ -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 { - 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()); diff --git a/crates/lumen-host/src/m0.rs b/crates/punktfunk-host/src/m0.rs similarity index 90% rename from crates/lumen-host/src/m0.rs rename to crates/punktfunk-host/src/m0.rs index 6682b0ad..3c46bbdd 100644 --- a/crates/lumen-host/src/m0.rs +++ b/crates/punktfunk-host/src/m0.rs @@ -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 { - 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" ); } } diff --git a/crates/lumen-host/src/m3.rs b/crates/punktfunk-host/src/m3.rs similarity index 86% rename from crates/lumen-host/src/m3.rs rename to crates/punktfunk-host/src/m3.rs index b35e3501..7a92ec6b 100644 --- a/crates/lumen-host/src/m3.rs +++ b/crates/punktfunk-host/src/m3.rs @@ -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, 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, 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, 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, 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, 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, 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, _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(); } diff --git a/crates/lumen-host/src/main.rs b/crates/punktfunk-host/src/main.rs similarity index 89% rename from crates/lumen-host/src/main.rs rename to crates/punktfunk-host/src/main.rs index 7d14f13c..f8128885 100644 --- a/crates/lumen-host/src/main.rs +++ b/crates/punktfunk-host/src/main.rs @@ -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 = 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 { } // 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 { 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 { 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 management API address (default: 127.0.0.1:47990) - --mgmt-token bearer token for the management API (or LUMEN_MGMT_TOKEN); + --mgmt-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 NVENC codec (default: h265) --bitrate target bitrate in Mbps (default: 20) --width --height synthetic source size (default: 1920x1080) - --out raw Annex-B output (default: /tmp/lumen-m0.) - --no-loopback skip the lumen_core round-trip verification + --out raw Annex-B output (default: /tmp/punktfunk-m0.) + --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." ); } diff --git a/crates/lumen-host/src/mgmt.rs b/crates/punktfunk-host/src/mgmt.rs similarity index 97% rename from crates/lumen-host/src/mgmt.rs rename to crates/punktfunk-host/src/mgmt.rs index 0407d866..1c673192 100644 --- a/crates/lumen-host/src/mgmt.rs +++ b/crates/punktfunk-host/src/mgmt.rs @@ -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, 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>, 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 { 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>) -> Json { 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" ); } } diff --git a/crates/lumen-host/src/pipeline.rs b/crates/punktfunk-host/src/pipeline.rs similarity index 84% rename from crates/lumen-host/src/pipeline.rs rename to crates/punktfunk-host/src/pipeline.rs index 3e3ce20b..71f338a3 100644 --- a/crates/lumen-host/src/pipeline.rs +++ b/crates/punktfunk-host/src/pipeline.rs @@ -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, diff --git a/crates/lumen-host/src/pwinit.rs b/crates/punktfunk-host/src/pwinit.rs similarity index 100% rename from crates/lumen-host/src/pwinit.rs rename to crates/punktfunk-host/src/pwinit.rs diff --git a/crates/lumen-host/src/vdisplay.rs b/crates/punktfunk-host/src/vdisplay.rs similarity index 92% rename from crates/lumen-host/src/vdisplay.rs rename to crates/punktfunk-host/src/vdisplay.rs index 028158d4..d74fb627 100644 --- a/crates/lumen-host/src/vdisplay.rs +++ b/crates/punktfunk-host/src/vdisplay.rs @@ -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; } -/// 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 { - 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 { 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" ) } } diff --git a/crates/lumen-host/src/vdisplay/gamescope.rs b/crates/punktfunk-host/src/vdisplay/gamescope.rs similarity index 90% rename from crates/lumen-host/src/vdisplay/gamescope.rs rename to crates/punktfunk-host/src/vdisplay/gamescope.rs index cf940f72..e5f70437 100644 --- a/crates/lumen-host/src/vdisplay/gamescope.rs +++ b/crates/punktfunk-host/src/vdisplay/gamescope.rs @@ -35,11 +35,11 @@ impl VirtualDisplay for GamescopeDisplay { fn create(&mut self, mode: Mode) -> Result { // Attach to an already-running gamescope (debug / Steam-launched session) instead of - // spawning one: LUMEN_GAMESCOPE_NODE=. - if let Ok(id) = std::env::var("LUMEN_GAMESCOPE_NODE") { + // spawning one: PUNKTFUNK_GAMESCOPE_NODE=. + 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 -- `. 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 { - 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 { .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 { /// Parse `stream available on node ID: N` from the spawned gamescope's log (ANSI-colored). fn node_from_log() -> Option { - 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()..]; diff --git a/crates/lumen-host/src/vdisplay/kwin.rs b/crates/punktfunk-host/src/vdisplay/kwin.rs similarity index 99% rename from crates/lumen-host/src/vdisplay/kwin.rs rename to crates/punktfunk-host/src/vdisplay/kwin.rs index b7251102..8d2a85eb 100644 --- a/crates/lumen-host/src/vdisplay/kwin.rs +++ b/crates/punktfunk-host/src/vdisplay/kwin.rs @@ -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-`. -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")?; diff --git a/crates/lumen-host/src/vdisplay/mutter.rs b/crates/punktfunk-host/src/vdisplay/mutter.rs similarity index 98% rename from crates/lumen-host/src/vdisplay/mutter.rs rename to crates/punktfunk-host/src/vdisplay/mutter.rs index f6473ac2..c899f174 100644 --- a/crates/lumen-host/src/vdisplay/mutter.rs +++ b/crates/punktfunk-host/src/vdisplay/mutter.rs @@ -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")?; diff --git a/crates/lumen-host/src/zerocopy/cuda.rs b/crates/punktfunk-host/src/zerocopy/cuda.rs similarity index 100% rename from crates/lumen-host/src/zerocopy/cuda.rs rename to crates/punktfunk-host/src/zerocopy/cuda.rs diff --git a/crates/lumen-host/src/zerocopy/egl.rs b/crates/punktfunk-host/src/zerocopy/egl.rs similarity index 100% rename from crates/lumen-host/src/zerocopy/egl.rs rename to crates/punktfunk-host/src/zerocopy/egl.rs diff --git a/crates/lumen-host/src/zerocopy/mod.rs b/crates/punktfunk-host/src/zerocopy/mod.rs similarity index 90% rename from crates/lumen-host/src/zerocopy/mod.rs rename to crates/punktfunk-host/src/zerocopy/mod.rs index b8d9aed7..0f6a81f5 100644 --- a/crates/lumen-host/src/zerocopy/mod.rs +++ b/crates/punktfunk-host/src/zerocopy/mod.rs @@ -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) } diff --git a/crates/lumen-host/src/zerocopy/vulkan.rs b/crates/punktfunk-host/src/zerocopy/vulkan.rs similarity index 100% rename from crates/lumen-host/src/zerocopy/vulkan.rs rename to crates/punktfunk-host/src/zerocopy/vulkan.rs diff --git a/include/lumen_core.h b/include/punktfunk_core.h similarity index 53% rename from include/lumen_core.h rename to include/punktfunk_core.h index b8a5f4c9..96a6c371 100644 --- a/include/lumen_core.h +++ b/include/punktfunk_core.h @@ -1,11 +1,11 @@ -/* lumen-core C ABI — see crates/lumen-core/src/abi.rs */ +/* punktfunk-core C ABI — see crates/punktfunk-core/src/abi.rs */ -#ifndef LUMEN_CORE_H -#define LUMEN_CORE_H +#ifndef PUNKTFUNK_CORE_H +#define PUNKTFUNK_CORE_H #pragma once -/* Generated by cbindgen from lumen-core. Do not edit by hand. */ +/* Generated by cbindgen from punktfunk-core. Do not edit by hand. */ #include #include @@ -13,7 +13,7 @@ #include // 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. #define ABI_VERSION 1 // 16-byte AEAD authentication tag appended by GCM. @@ -25,52 +25,52 @@ // Fixed serialized size of an [`InputEvent`] on the wire (tag + fields). #define INPUT_WIRE_LEN (((((1 + 1) + 4) + 4) + 4) + 4) -#define LUMEN_BTN_DPAD_UP 1 +#define PUNKTFUNK_BTN_DPAD_UP 1 -#define LUMEN_BTN_DPAD_DOWN 2 +#define PUNKTFUNK_BTN_DPAD_DOWN 2 -#define LUMEN_BTN_DPAD_LEFT 4 +#define PUNKTFUNK_BTN_DPAD_LEFT 4 -#define LUMEN_BTN_DPAD_RIGHT 8 +#define PUNKTFUNK_BTN_DPAD_RIGHT 8 -#define LUMEN_BTN_START 16 +#define PUNKTFUNK_BTN_START 16 -#define LUMEN_BTN_BACK 32 +#define PUNKTFUNK_BTN_BACK 32 -#define LUMEN_BTN_LS_CLICK 64 +#define PUNKTFUNK_BTN_LS_CLICK 64 -#define LUMEN_BTN_RS_CLICK 128 +#define PUNKTFUNK_BTN_RS_CLICK 128 -#define LUMEN_BTN_LB 256 +#define PUNKTFUNK_BTN_LB 256 -#define LUMEN_BTN_RB 512 +#define PUNKTFUNK_BTN_RB 512 -#define LUMEN_BTN_GUIDE 1024 +#define PUNKTFUNK_BTN_GUIDE 1024 -#define LUMEN_BTN_A 4096 +#define PUNKTFUNK_BTN_A 4096 -#define LUMEN_BTN_B 8192 +#define PUNKTFUNK_BTN_B 8192 -#define LUMEN_BTN_X 16384 +#define PUNKTFUNK_BTN_X 16384 -#define LUMEN_BTN_Y 32768 +#define PUNKTFUNK_BTN_Y 32768 // Axis ids for `InputKind::GamepadAxis`. -#define LUMEN_AXIS_LS_X 0 +#define PUNKTFUNK_AXIS_LS_X 0 -#define LUMEN_AXIS_LS_Y 1 +#define PUNKTFUNK_AXIS_LS_Y 1 -#define LUMEN_AXIS_RS_X 2 +#define PUNKTFUNK_AXIS_RS_X 2 -#define LUMEN_AXIS_RS_Y 3 +#define PUNKTFUNK_AXIS_RS_Y 3 // Triggers: value range 0..255. -#define LUMEN_AXIS_LT 4 +#define PUNKTFUNK_AXIS_LT 4 -#define LUMEN_AXIS_RT 5 +#define PUNKTFUNK_AXIS_RT 5 -// Identifies a lumen video packet (vs. an input datagram, see [`crate::input`]). -#define LUMEN_MAGIC 201 +// Identifies a punktfunk video packet (vs. an input datagram, see [`crate::input`]). +#define PUNKTFUNK_MAGIC 201 #define FLAG_PIC 1 @@ -82,97 +82,97 @@ // `shard_payload` so `HEADER_LEN + shard_payload + CRYPTO_OVERHEAD ≤ MAX_DATAGRAM_BYTES`. #define MAX_DATAGRAM_BYTES 2048 -#if defined(LUMEN_FEATURE_QUIC) +#if defined(PUNKTFUNK_FEATURE_QUIC) // Datagram wire tags. Video rides UDP; everything low-rate rides QUIC datagrams, // demultiplexed by the first byte: input = [`crate::input::INPUT_MAGIC`] (0xC8), // audio = [`AUDIO_MAGIC`], rumble = [`RUMBLE_MAGIC`]. -#define LUMEN_AUDIO_MAGIC 201 +#define PUNKTFUNK_AUDIO_MAGIC 201 #endif -#if defined(LUMEN_FEATURE_QUIC) -#define LUMEN_RUMBLE_MAGIC 202 +#if defined(PUNKTFUNK_FEATURE_QUIC) +#define PUNKTFUNK_RUMBLE_MAGIC 202 #endif // 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. -enum LumenStatus +enum PunktfunkStatus #if defined(__cplusplus) || __STDC_VERSION__ >= 202311L : int32_t #endif // defined(__cplusplus) || __STDC_VERSION__ >= 202311L { - LUMEN_STATUS_OK = 0, - LUMEN_STATUS_INVALID_ARG = -1, - LUMEN_STATUS_FEC = -2, - LUMEN_STATUS_CRYPTO = -3, - LUMEN_STATUS_BAD_PACKET = -4, - LUMEN_STATUS_NO_FRAME = -5, - LUMEN_STATUS_UNSUPPORTED = -6, - LUMEN_STATUS_IO = -7, - LUMEN_STATUS_NULL_POINTER = -8, - LUMEN_STATUS_TIMEOUT = -9, - LUMEN_STATUS_CLOSED = -10, - LUMEN_STATUS_PANIC = -99, + PUNKTFUNK_STATUS_OK = 0, + PUNKTFUNK_STATUS_INVALID_ARG = -1, + PUNKTFUNK_STATUS_FEC = -2, + PUNKTFUNK_STATUS_CRYPTO = -3, + PUNKTFUNK_STATUS_BAD_PACKET = -4, + PUNKTFUNK_STATUS_NO_FRAME = -5, + PUNKTFUNK_STATUS_UNSUPPORTED = -6, + PUNKTFUNK_STATUS_IO = -7, + PUNKTFUNK_STATUS_NULL_POINTER = -8, + PUNKTFUNK_STATUS_TIMEOUT = -9, + PUNKTFUNK_STATUS_CLOSED = -10, + PUNKTFUNK_STATUS_PANIC = -99, }; #ifndef __cplusplus #if __STDC_VERSION__ >= 202311L -typedef enum LumenStatus LumenStatus; +typedef enum PunktfunkStatus PunktfunkStatus; #else -typedef int32_t LumenStatus; +typedef int32_t PunktfunkStatus; #endif // __STDC_VERSION__ >= 202311L #endif // __cplusplus // Kinds of input event. `#[repr(u8)]` so it crosses the C ABI as a byte tag. -enum LumenInputKind +enum PunktfunkInputKind #if defined(__cplusplus) || __STDC_VERSION__ >= 202311L : uint8_t #endif // defined(__cplusplus) || __STDC_VERSION__ >= 202311L { - LUMEN_INPUT_KIND_KEY_DOWN = 0, - LUMEN_INPUT_KIND_KEY_UP = 1, + PUNKTFUNK_INPUT_KIND_KEY_DOWN = 0, + PUNKTFUNK_INPUT_KIND_KEY_UP = 1, // Relative motion: `x`/`y` carry `dx`/`dy`. - LUMEN_INPUT_KIND_MOUSE_MOVE = 2, + PUNKTFUNK_INPUT_KIND_MOUSE_MOVE = 2, // Absolute motion: `x`/`y` carry pixel coordinates. - LUMEN_INPUT_KIND_MOUSE_MOVE_ABS = 3, - LUMEN_INPUT_KIND_MOUSE_BUTTON_DOWN = 4, - LUMEN_INPUT_KIND_MOUSE_BUTTON_UP = 5, + PUNKTFUNK_INPUT_KIND_MOUSE_MOVE_ABS = 3, + PUNKTFUNK_INPUT_KIND_MOUSE_BUTTON_DOWN = 4, + PUNKTFUNK_INPUT_KIND_MOUSE_BUTTON_UP = 5, // `x` carries the (signed) scroll delta. - LUMEN_INPUT_KIND_MOUSE_SCROLL = 6, + PUNKTFUNK_INPUT_KIND_MOUSE_SCROLL = 6, // `code` = button bit ([`gamepad`] `BTN_*`), `x` ≠ 0 = pressed, `flags` = pad index. - LUMEN_INPUT_KIND_GAMEPAD_BUTTON = 7, + PUNKTFUNK_INPUT_KIND_GAMEPAD_BUTTON = 7, // `code` = axis id ([`gamepad`] `AXIS_*`), `x` = axis value, `flags` = pad index. // Sticks are i16 range (−32768..32767) in the XInput/Moonlight convention — **+y = // up** (unlike mouse coordinates); triggers 0..255. - LUMEN_INPUT_KIND_GAMEPAD_AXIS = 8, + PUNKTFUNK_INPUT_KIND_GAMEPAD_AXIS = 8, }; #ifndef __cplusplus #if __STDC_VERSION__ >= 202311L -typedef enum LumenInputKind LumenInputKind; +typedef enum PunktfunkInputKind PunktfunkInputKind; #else -typedef uint8_t LumenInputKind; +typedef uint8_t PunktfunkInputKind; #endif // __STDC_VERSION__ >= 202311L #endif // __cplusplus -#if defined(LUMEN_FEATURE_QUIC) -// Opaque handle to a live `lumen/1` connection (QUIC control plane + UDP data plane, all +#if defined(PUNKTFUNK_FEATURE_QUIC) +// 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`) // may be pulled from its own thread, at most one thread per plane. The accessors only // take shared references internally (per-plane mutexed borrow slots), so cross-plane // concurrency is sound — never two threads on the *same* plane. -typedef struct LumenConnection LumenConnection; +typedef struct PunktfunkConnection PunktfunkConnection; #endif // Opaque session handle. Pointer-only from C. -typedef struct LumenSession LumenSession; +typedef struct PunktfunkSession PunktfunkSession; // 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. typedef struct { uint32_t struct_size; // 0 = host, 1 = client. uint32_t role; - // 1 = P1 (GameStream-compatible), 2 = P2 (`lumen/1`). + // 1 = P1 (GameStream-compatible), 2 = P2 (`punktfunk/1`). uint32_t phase; // 0 = GF(2⁸), 1 = GF(2¹⁶). uint32_t fec_scheme; @@ -187,22 +187,22 @@ typedef struct { uint32_t loopback_drop_period; // Largest encoded access unit the receiver will accept (bounds reassembler memory). uint64_t max_frame_bytes; -} LumenConfig; +} PunktfunkConfig; // 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. typedef struct { const uint8_t *data; uintptr_t len; uint32_t frame_index; uint64_t pts_ns; uint32_t flags; -} LumenFrame; +} PunktfunkFrame; // A single input event. `#[repr(C)]` — shared verbatim with the C ABI as -// `LumenInputEvent`. +// `PunktfunkInputEvent`. typedef struct { - LumenInputKind kind; + PunktfunkInputKind kind; uint8_t _pad[3]; // keycode / button id / axis id, depending on `kind`. uint32_t code; @@ -212,7 +212,7 @@ typedef struct { int32_t y; // modifier bitmask or gamepad index. uint32_t flags; -} LumenInputEvent; +} PunktfunkInputEvent; // Snapshot of session counters. typedef struct { @@ -225,17 +225,17 @@ typedef struct { uint64_t fec_recovered_shards; uint64_t bytes_sent; uint64_t bytes_received; -} LumenStats; +} PunktfunkStats; -#if defined(LUMEN_FEATURE_QUIC) -// 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. +#if defined(PUNKTFUNK_FEATURE_QUIC) +// 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. typedef struct { const uint8_t *data; uintptr_t len; uint32_t seq; uint64_t pts_ns; -} LumenAudioPacket; +} PunktfunkAudioPacket; #endif #ifdef __cplusplus @@ -243,77 +243,80 @@ extern "C" { #endif // __cplusplus // Current ABI version. Mismatch with [`crate::ABI_VERSION`] means incompatible core. -uint32_t lumen_abi_version(void); +uint32_t punktfunk_abi_version(void); // Create a session over a real UDP transport (`local`/`peer` are `host:port` strings). // Returns NULL on error. // // # Safety // `cfg`, `local`, `peer` must be valid pointers; the strings must be NUL-terminated. -LumenSession *lumen_session_new(const LumenConfig *cfg, const char *local, const char *peer); +PunktfunkSession *punktfunk_session_new(const PunktfunkConfig *cfg, + const char *local, + const char *peer); // Create a connected host+client session pair sharing an in-process loopback // transport. Test/dev only — exercises the full FEC + framing path without a network. // // # Safety // All four pointers must be valid; the two out-params receive owned handles. -LumenStatus lumen_test_loopback_pair(const LumenConfig *host_cfg, - const LumenConfig *client_cfg, - LumenSession **out_host, - LumenSession **out_client); +PunktfunkStatus punktfunk_test_loopback_pair(const PunktfunkConfig *host_cfg, + const PunktfunkConfig *client_cfg, + PunktfunkSession **out_host, + PunktfunkSession **out_client); // 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. -void lumen_session_free(LumenSession *s); +// `s` must be a handle from `punktfunk_session_new`/`punktfunk_test_loopback_pair`, freed once. +void punktfunk_session_free(PunktfunkSession *s); // Host: FEC-protect, packetize, seal and send one encoded access unit. // // # Safety // `s` is a valid host handle; `data` points to `len` readable bytes (or `len == 0`). -LumenStatus lumen_host_submit_frame(LumenSession *s, - const uint8_t *data, - uintptr_t len, - uint64_t pts_ns, - uint32_t flags); +PunktfunkStatus punktfunk_host_submit_frame(PunktfunkSession *s, + const uint8_t *data, + uintptr_t len, + uint64_t pts_ns, + uint32_t flags); -// 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`. -LumenStatus lumen_client_poll_frame(LumenSession *s, LumenFrame *out); +// `s` is a valid client handle; `out` points to a writable `PunktfunkFrame`. +PunktfunkStatus punktfunk_client_poll_frame(PunktfunkSession *s, PunktfunkFrame *out); // Client: serialize and send one input event to the host. // // # Safety // `s` is a valid client handle; `ev` points to a valid [`InputEvent`]. -LumenStatus lumen_send_input(LumenSession *s, const LumenInputEvent *ev); +PunktfunkStatus punktfunk_send_input(PunktfunkSession *s, const PunktfunkInputEvent *ev); // 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`. -LumenStatus lumen_set_input_callback(LumenSession *s, void (*cb)(const LumenInputEvent *event, - void *user), void *user); +PunktfunkStatus punktfunk_set_input_callback(PunktfunkSession *s, + void (*cb)(const PunktfunkInputEvent *event, void *user), + void *user); // 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. -int32_t lumen_host_poll_input(LumenSession *s); +int32_t punktfunk_host_poll_input(PunktfunkSession *s); // Copy session counters into `*out`. // // # Safety -// `s` is a valid handle; `out` points to a writable `LumenStats`. -LumenStatus lumen_get_stats(LumenSession *s, LumenStats *out); +// `s` is a valid handle; `out` points to a writable `PunktfunkStats`. +PunktfunkStatus punktfunk_get_stats(PunktfunkSession *s, PunktfunkStats *out); -#if defined(LUMEN_FEATURE_QUIC) -// Connect to a `lumen/1` host and start a session at `width`x`height`@`refresh_hz`. +#if defined(PUNKTFUNK_FEATURE_QUIC) +// 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 @@ -324,31 +327,33 @@ LumenStatus lumen_get_stats(LumenSession *s, LumenStats *out); // # Safety // `host` is a NUL-terminated UTF-8 string (IP or hostname resolvable by the platform); // `pin_sha256`/`observed_sha256_out` are each NULL or valid for 32 bytes. -LumenConnection *lumen_connect(const char *host, - uint16_t port, - uint32_t width, - uint32_t height, - uint32_t refresh_hz, - const uint8_t *pin_sha256, - uint8_t *observed_sha256_out, - uint32_t timeout_ms); +PunktfunkConnection *punktfunk_connect(const char *host, + uint16_t port, + uint32_t width, + uint32_t height, + uint32_t refresh_hz, + const uint8_t *pin_sha256, + uint8_t *observed_sha256_out, + uint32_t timeout_ms); #endif -#if defined(LUMEN_FEATURE_QUIC) +#if defined(PUNKTFUNK_FEATURE_QUIC) // 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). // // # Safety // `c` is a valid connection handle; `out` is writable. At most one thread pulls video — // it may run concurrently with one audio-pulling and one rumble-pulling thread. -LumenStatus lumen_connection_next_au(LumenConnection *c, LumenFrame *out, uint32_t timeout_ms); +PunktfunkStatus punktfunk_connection_next_au(PunktfunkConnection *c, + PunktfunkFrame *out, + uint32_t timeout_ms); #endif -#if defined(LUMEN_FEATURE_QUIC) +#if defined(PUNKTFUNK_FEATURE_QUIC) // 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. @@ -356,55 +361,56 @@ LumenStatus lumen_connection_next_au(LumenConnection *c, LumenFrame *out, uint32 // # Safety // `c` is a valid connection handle; `out` is writable. At most one thread pulls audio — // it may run concurrently with the video/rumble pullers. -LumenStatus lumen_connection_next_audio(LumenConnection *c, - LumenAudioPacket *out, - uint32_t timeout_ms); +PunktfunkStatus punktfunk_connection_next_audio(PunktfunkConnection *c, + PunktfunkAudioPacket *out, + uint32_t timeout_ms); #endif -#if defined(LUMEN_FEATURE_QUIC) +#if defined(PUNKTFUNK_FEATURE_QUIC) // 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. -LumenStatus lumen_connection_next_rumble(LumenConnection *c, - uint16_t *pad, - uint16_t *low, - uint16_t *high, - uint32_t timeout_ms); +PunktfunkStatus punktfunk_connection_next_rumble(PunktfunkConnection *c, + uint16_t *pad, + uint16_t *low, + uint16_t *high, + uint32_t timeout_ms); #endif -#if defined(LUMEN_FEATURE_QUIC) +#if defined(PUNKTFUNK_FEATURE_QUIC) // Send one input event to the host as a QUIC datagram (non-blocking enqueue). // // # Safety // `c` is a valid connection handle; `ev` points to a valid [`InputEvent`]. -LumenStatus lumen_connection_send_input(LumenConnection *c, const LumenInputEvent *ev); +PunktfunkStatus punktfunk_connection_send_input(PunktfunkConnection *c, + const PunktfunkInputEvent *ev); #endif -#if defined(LUMEN_FEATURE_QUIC) +#if defined(PUNKTFUNK_FEATURE_QUIC) // The host-confirmed session mode (from the Welcome). Safe any time after connect. // // # Safety // `c` is a valid connection handle; out pointers are writable (NULLs are skipped). -LumenStatus lumen_connection_mode(const LumenConnection *c, - uint32_t *width, - uint32_t *height, - uint32_t *refresh_hz); +PunktfunkStatus punktfunk_connection_mode(const PunktfunkConnection *c, + uint32_t *width, + uint32_t *height, + uint32_t *refresh_hz); #endif -#if defined(LUMEN_FEATURE_QUIC) +#if defined(PUNKTFUNK_FEATURE_QUIC) // 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. -void lumen_connection_close(LumenConnection *c); +// `c` was returned by [`punktfunk_connect`] and is not used after this call. +void punktfunk_connection_close(PunktfunkConnection *c); #endif #ifdef __cplusplus } // extern "C" #endif // __cplusplus -#endif /* LUMEN_CORE_H */ +#endif /* PUNKTFUNK_CORE_H */ diff --git a/scripts/60-lumen.rules b/scripts/60-punktfunk.rules similarity index 76% rename from scripts/60-lumen.rules rename to scripts/60-punktfunk.rules index d68b4b7b..ca99b527 100644 --- a/scripts/60-lumen.rules +++ b/scripts/60-punktfunk.rules @@ -1,11 +1,11 @@ -# udev rules for the lumen streaming host (mirrors Sunshine's 60-sunshine.rules). +# udev rules for the punktfunk streaming host (mirrors Sunshine's 60-sunshine.rules). # # Grants the `input` group access to /dev/uinput so the host can create virtual gamepads # (one X-Box-360-class pad per connected Moonlight controller). `static_node` makes the node # exist before the uinput module loads. # # Install: -# sudo cp scripts/60-lumen.rules /etc/udev/rules.d/ +# sudo cp scripts/60-punktfunk.rules /etc/udev/rules.d/ # sudo usermod -aG input $USER # then re-login (or reboot) # sudo udevadm control --reload-rules && sudo udevadm trigger KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", GROUP="input", MODE="0660", TAG+="uaccess" diff --git a/scripts/bootstrap-ubuntu.sh b/scripts/bootstrap-ubuntu.sh index 31116006..8951ad01 100755 --- a/scripts/bootstrap-ubuntu.sh +++ b/scripts/bootstrap-ubuntu.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Bootstrap an Ubuntu (24.04 "noble") NVIDIA-GPU VM to build/run the lumen Linux host +# Bootstrap an Ubuntu (24.04 "noble") NVIDIA-GPU VM to build/run the punktfunk Linux host # and the M0 capture spike (headless Sway/wlroots -> PipeWire -> NVENC). # # Assumes the NVIDIA driver + an FFmpeg-with-NVENC are ALREADY installed (verify-only). @@ -57,10 +57,10 @@ if have ffmpeg; then ok "FFmpeg has NVENC: $(ffmpeg -hide_banner -encoders 2>/dev/null | grep -oE '(hevc|h264)_nvenc' | paste -sd' ' -)" log " smoke-test: 1s HEVC NVENC encode to null" if ffmpeg -hide_banner -loglevel error -f lavfi -i color=c=black:s=1280x720:d=1 \ - -c:v hevc_nvenc -preset p1 -tune ull -f null - 2>/tmp/lumen_nvenc.err; then + -c:v hevc_nvenc -preset p1 -tune ull -f null - 2>/tmp/punktfunk_nvenc.err; then ok "hevc_nvenc encode succeeded — NVENC is usable in this guest" else - warn "hevc_nvenc encode FAILED (see /tmp/lumen_nvenc.err). Common cause on a VM: \ + warn "hevc_nvenc encode FAILED (see /tmp/punktfunk_nvenc.err). Common cause on a VM: \ missing libnvidia-encode.so.1 or an unlicensed vGPU." fi else diff --git a/scripts/build-xcframework.sh b/scripts/build-xcframework.sh index fef7eb06..8dff1afd 100644 --- a/scripts/build-xcframework.sh +++ b/scripts/build-xcframework.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash -# Build LumenCore.xcframework for the Apple clients — run ON A MAC with Xcode + rustup. +# Build PunktfunkCore.xcframework for the Apple clients — run ON A MAC with Xcode + rustup. # # rustup target add aarch64-apple-darwin x86_64-apple-darwin # + aarch64-apple-ios for iOS # bash scripts/build-xcframework.sh # -# Output: clients/apple/LumenCore.xcframework (consumed by clients/apple/Package.swift). -# The library is built WITH the `quic` feature (the lumen/1 connection API), so the bundled -# header gets LUMEN_FEATURE_QUIC pre-defined — Swift sees lumen_connect & co. unconditionally. +# Output: clients/apple/PunktfunkCore.xcframework (consumed by clients/apple/Package.swift). +# The library is built WITH the `quic` feature (the punktfunk/1 connection API), so the bundled +# header gets PUNKTFUNK_FEATURE_QUIC pre-defined — Swift sees punktfunk_connect & co. unconditionally. set -euo pipefail cd "$(dirname "$0")/.." @@ -16,10 +16,10 @@ BUILD_IOS="${BUILD_IOS:-0}" # BUILD_IOS=1 adds an iOS slice (requires the ios ta # Deployment targets must match Package.swift's platforms, or every consumer link emits # "object file was built for newer macOS version" warnings. for t in "${TARGETS_MAC[@]}"; do - MACOSX_DEPLOYMENT_TARGET=14.0 cargo build --release -p lumen-core --features quic --target "$t" + MACOSX_DEPLOYMENT_TARGET=14.0 cargo build --release -p punktfunk-core --features quic --target "$t" done if [[ "$BUILD_IOS" == "1" ]]; then - IPHONEOS_DEPLOYMENT_TARGET=17.0 cargo build --release -p lumen-core --features quic --target aarch64-apple-ios + IPHONEOS_DEPLOYMENT_TARGET=17.0 cargo build --release -p punktfunk-core --features quic --target aarch64-apple-ios fi STAGE="$(mktemp -d)" @@ -28,33 +28,33 @@ trap 'rm -rf "$STAGE"' EXIT # Universal macOS static lib. mkdir -p "$STAGE/macos" lipo -create \ - target/aarch64-apple-darwin/release/liblumen_core.a \ - target/x86_64-apple-darwin/release/liblumen_core.a \ - -output "$STAGE/macos/liblumen_core.a" + target/aarch64-apple-darwin/release/libpunktfunk_core.a \ + target/x86_64-apple-darwin/release/libpunktfunk_core.a \ + -output "$STAGE/macos/libpunktfunk_core.a" # Headers dir: the generated C header (with the quic API force-enabled) + a modulemap so -# Swift can `import LumenCore`. +# Swift can `import PunktfunkCore`. mkdir -p "$STAGE/include" { - echo "#define LUMEN_FEATURE_QUIC 1" - cat include/lumen_core.h -} > "$STAGE/include/lumen_core.h" + echo "#define PUNKTFUNK_FEATURE_QUIC 1" + cat include/punktfunk_core.h +} > "$STAGE/include/punktfunk_core.h" cat > "$STAGE/include/module.modulemap" <<'EOF' -module LumenCore { - header "lumen_core.h" +module PunktfunkCore { + header "punktfunk_core.h" export * } EOF -ARGS=(-library "$STAGE/macos/liblumen_core.a" -headers "$STAGE/include") +ARGS=(-library "$STAGE/macos/libpunktfunk_core.a" -headers "$STAGE/include") if [[ "$BUILD_IOS" == "1" ]]; then - ARGS+=(-library target/aarch64-apple-ios/release/liblumen_core.a -headers "$STAGE/include") + ARGS+=(-library target/aarch64-apple-ios/release/libpunktfunk_core.a -headers "$STAGE/include") fi # Cargo does NOT fingerprint MACOSX_DEPLOYMENT_TARGET — units cached from a build without # it keep their old minos forever. Refuse to ship anything newer than the package floor # (objects BELOW it, e.g. rustup's precompiled std at 11.0, are fine and unavoidable). -for obj in "$STAGE"/macos/liblumen_core.a; do +for obj in "$STAGE"/macos/libpunktfunk_core.a; do bad=$(otool -l "$obj" 2>/dev/null | awk '/minos/ {print $2}' | sort -uV | awk -F. '$1 > 14' | head -1) if [[ -n "$bad" ]]; then echo "ERROR: $obj contains objects built for macOS $bad (> 14.0)." >&2 @@ -63,6 +63,6 @@ for obj in "$STAGE"/macos/liblumen_core.a; do fi done -rm -rf clients/apple/LumenCore.xcframework -xcodebuild -create-xcframework "${ARGS[@]}" -output clients/apple/LumenCore.xcframework -echo "OK: clients/apple/LumenCore.xcframework" +rm -rf clients/apple/PunktfunkCore.xcframework +xcodebuild -create-xcframework "${ARGS[@]}" -output clients/apple/PunktfunkCore.xcframework +echo "OK: clients/apple/PunktfunkCore.xcframework" diff --git a/scripts/headless/capture-smoke-test.sh b/scripts/headless/capture-smoke-test.sh index 32fe8228..cb2839de 100755 --- a/scripts/headless/capture-smoke-test.sh +++ b/scripts/headless/capture-smoke-test.sh @@ -7,7 +7,7 @@ # Ctrl-C to stop; then play/inspect the file (e.g. ffprobe out.mkv). set -euo pipefail -OUT="${1:-/tmp/lumen-headless-test.mkv}" +OUT="${1:-/tmp/punktfunk-headless-test.mkv}" : "${WAYLAND_DISPLAY:?set WAYLAND_DISPLAY (e.g. wayland-1) — is headless Sway running?}" : "${XDG_RUNTIME_DIR:?set XDG_RUNTIME_DIR=/run/user/\$(id -u)}" diff --git a/scripts/headless/env.sh b/scripts/headless/env.sh index 36f33063..ba7a25cb 100644 --- a/scripts/headless/env.sh +++ b/scripts/headless/env.sh @@ -1,5 +1,5 @@ # shellcheck shell=bash -# Source before launching headless Sway / the lumen host on an NVIDIA VM: +# Source before launching headless Sway / the punktfunk host on an NVIDIA VM: # source scripts/headless/env.sh # These are the wlroots-on-NVIDIA workarounds the research turned up (gles2 is the # known-good renderer; Vulkan is flaky on the proprietary driver — try it only later). diff --git a/scripts/headless/prepare-session.sh b/scripts/headless/prepare-session.sh index 434c9a00..411a8e30 100755 --- a/scripts/headless/prepare-session.sh +++ b/scripts/headless/prepare-session.sh @@ -2,8 +2,8 @@ # Run AFTER headless Sway is up (run-headless-sway.sh), from a second shell on the same # user. It: (1) points this shell at the running Sway, (2) gives HEADLESS-1 a real refresh # clock (an idle/0 mHz output produces no frames), (3) imports the env the ScreenCast portal -# needs to find Sway and pick the wlr backend, and (4) writes /tmp/lumen-sway-env.sh so -# other shells (e.g. `cargo run -p lumen-host`) can `source` it. +# needs to find Sway and pick the wlr backend, and (4) writes /tmp/punktfunk-sway-env.sh so +# other shells (e.g. `cargo run -p punktfunk-host`) can `source` it. # # Usage: bash scripts/headless/prepare-session.sh [WxH@RHz] (default 1920x1080@60Hz) set -euo pipefail @@ -32,7 +32,7 @@ echo "HEADLESS-1 set to $MODE" systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP SWAYSOCK XDG_RUNTIME_DIR 2>/dev/null || true dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP SWAYSOCK 2>/dev/null || true -cat > /tmp/lumen-sway-env.sh < /tmp/punktfunk-sway-env.sh </dev/null 2>&1 || true # rebuild the menu cache under the correct env +plasmashell & + +echo "headless KDE up on $WAYLAND_DISPLAY ($RES), kwin pid $KWIN_PID" +wait "$KWIN_PID" diff --git a/scripts/headless/run-headless-sway.sh b/scripts/headless/run-headless-sway.sh index 2b9c28c0..12c05595 100755 --- a/scripts/headless/run-headless-sway.sh +++ b/scripts/headless/run-headless-sway.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash -# Launch headless Sway on the NVIDIA box for the lumen M0 capture spike. +# Launch headless Sway on the NVIDIA box for the punktfunk M0 capture spike. # # Runs on the user's *shared* session bus (NOT a private dbus-run-session) so that the -# ScreenCast portal (xdg-desktop-portal-wlr) and the lumen host share one bus. After this +# ScreenCast portal (xdg-desktop-portal-wlr) and the punktfunk host share one bus. After this # is up, run `prepare-session.sh` from a second shell to set the mode + portal env. # # Prereqs (see docs/linux-setup.md / scripts/bootstrap-ubuntu.sh): diff --git a/scripts/headless/sway.config b/scripts/headless/sway.config index 753910b2..37e20762 100644 --- a/scripts/headless/sway.config +++ b/scripts/headless/sway.config @@ -1,4 +1,4 @@ -# Minimal headless Sway config for the lumen M0 capture spike. +# Minimal headless Sway config for the punktfunk M0 capture spike. # Under WLR_BACKENDS=headless, Sway 1.9 auto-creates one output named HEADLESS-1 # (fixed 1920x1080); this just resizes it. For extra outputs: `swaymsg create_output` # (auto-named HEADLESS-2, ...). Set the resolution to your target client size. diff --git a/scripts/host.env.example b/scripts/host.env.example index 0f210434..5a0adf38 100644 --- a/scripts/host.env.example +++ b/scripts/host.env.example @@ -1,4 +1,4 @@ -# lumen host configuration (~/.config/lumen/host.env) — consumed by lumen-host.service. +# punktfunk host configuration (~/.config/punktfunk/host.env) — consumed by punktfunk-host.service. # Session / compositor environment (headless KWin example). XDG_RUNTIME_DIR=/run/user/1000 @@ -8,15 +8,15 @@ XDG_CURRENT_DESKTOP=KDE # Video source: `virtual` creates a per-client virtual output at the client's exact # resolution+refresh (the flagship mode); `portal` captures an existing monitor. -LUMEN_VIDEO_SOURCE=virtual +PUNKTFUNK_VIDEO_SOURCE=virtual # GPU zero-copy capture (EGL/Vulkan → CUDA → NVENC). Falls back to CPU automatically. -LUMEN_ZEROCOPY=1 +PUNKTFUNK_ZEROCOPY=1 # Optional overrides (apps.json is the primary mechanism for per-app settings): -#LUMEN_COMPOSITOR=kwin # kwin | mutter | gamescope | wlroots -#LUMEN_GAMESCOPE_APP=vkcube # nested command for ad-hoc gamescope sessions -#LUMEN_INPUT_BACKEND=libei # wlr | libei | gamescope | uinput -#LUMEN_FEC_PCT=20 # video FEC overhead percent -#LUMEN_PERF=1 # per-stage timing logs +#PUNKTFUNK_COMPOSITOR=kwin # kwin | mutter | gamescope | wlroots +#PUNKTFUNK_GAMESCOPE_APP=vkcube # nested command for ad-hoc gamescope sessions +#PUNKTFUNK_INPUT_BACKEND=libei # wlr | libei | gamescope | uinput +#PUNKTFUNK_FEC_PCT=20 # video FEC overhead percent +#PUNKTFUNK_PERF=1 # per-stage timing logs #RUST_LOG=info diff --git a/scripts/lumen-host.service b/scripts/lumen-host.service deleted file mode 100644 index 754358f3..00000000 --- a/scripts/lumen-host.service +++ /dev/null @@ -1,21 +0,0 @@ -# lumen streaming host — systemd USER unit. -# -# Install: -# mkdir -p ~/.config/systemd/user && cp scripts/lumen-host.service ~/.config/systemd/user/ -# cp scripts/host.env.example ~/.config/lumen/host.env # then edit -# systemctl --user daemon-reload && systemctl --user enable --now lumen-host -# -# The unit assumes the compositor session (e.g. headless KWin on wayland-kde) is already up; -# for a fully self-contained appliance, pair it with a kwin_wayland user unit it can After=. -[Unit] -Description=lumen GameStream host -After=pipewire.service - -[Service] -EnvironmentFile=%h/.config/lumen/host.env -ExecStart=%h/lumen/target/release/lumen-host serve -Restart=on-failure -RestartSec=2 - -[Install] -WantedBy=default.target diff --git a/scripts/punktfunk-host.service b/scripts/punktfunk-host.service new file mode 100644 index 00000000..f168d3d3 --- /dev/null +++ b/scripts/punktfunk-host.service @@ -0,0 +1,21 @@ +# punktfunk streaming host — systemd USER unit. +# +# Install: +# mkdir -p ~/.config/systemd/user && cp scripts/punktfunk-host.service ~/.config/systemd/user/ +# cp scripts/host.env.example ~/.config/punktfunk/host.env # then edit +# systemctl --user daemon-reload && systemctl --user enable --now punktfunk-host +# +# The unit assumes the compositor session (e.g. headless KWin on wayland-kde) is already up; +# for a fully self-contained appliance, pair it with a kwin_wayland user unit it can After=. +[Unit] +Description=punktfunk GameStream host +After=pipewire.service + +[Service] +EnvironmentFile=%h/.config/punktfunk/host.env +ExecStart=%h/punktfunk/target/release/punktfunk-host serve +Restart=on-failure +RestartSec=2 + +[Install] +WantedBy=default.target diff --git a/tools/loss-harness/Cargo.toml b/tools/loss-harness/Cargo.toml index fc8d7fd2..3c8e034f 100644 --- a/tools/loss-harness/Cargo.toml +++ b/tools/loss-harness/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "loss-harness" -description = "FEC loss-resilience sweep over the lumen-core loopback (plan §10)" +description = "FEC loss-resilience sweep over the punktfunk-core loopback (plan §10)" version.workspace = true edition.workspace = true rust-version.workspace = true @@ -9,4 +9,4 @@ authors.workspace = true repository.workspace = true [dependencies] -lumen-core = { path = "../../crates/lumen-core" } +punktfunk-core = { path = "../../crates/punktfunk-core" } diff --git a/tools/loss-harness/src/main.rs b/tools/loss-harness/src/main.rs index cac7a713..d46ffcb9 100644 --- a/tools/loss-harness/src/main.rs +++ b/tools/loss-harness/src/main.rs @@ -2,20 +2,20 @@ //! //! Drives access units through the in-process loopback at increasing loss rates, for //! both FEC schemes, and prints how many frames survive. A pure-software stand-in for -//! `tc netem` that needs no network and runs anywhere `lumen_core` builds. The real M3 +//! `tc netem` that needs no network and runs anywhere `punktfunk_core` builds. The real M3 //! harness adds `tc netem` jitter/reorder on the UDP path. -use lumen_core::config::{Config, FecConfig, FecScheme, ProtocolPhase, Role}; -use lumen_core::error::LumenError; -use lumen_core::session::Session; -use lumen_core::transport::loopback_pair; +use punktfunk_core::config::{Config, FecConfig, FecScheme, ProtocolPhase, Role}; +use punktfunk_core::error::PunktfunkError; +use punktfunk_core::session::Session; +use punktfunk_core::transport::loopback_pair; fn config(role: Role, scheme: FecScheme, drop_period: u32) -> Config { Config { role, phase: match scheme { FecScheme::Gf8 => ProtocolPhase::P1GameStream, - FecScheme::Gf16 => ProtocolPhase::P2Lumen, + FecScheme::Gf16 => ProtocolPhase::P2Punktfunk, }, fec: FecConfig { scheme, @@ -47,7 +47,7 @@ fn run(scheme: FecScheme, drop_period: u32, frames: usize, frame_len: usize) -> completed += 1; } } - Err(LumenError::NoFrame) => {} // unrecoverable at this loss rate + Err(PunktfunkError::NoFrame) => {} // unrecoverable at this loss rate Err(e) => panic!("unexpected error: {e}"), } } @@ -59,7 +59,7 @@ fn main() { let frame_len = 100_000; // ~98 shards across 2 FEC blocks let periods = [0u32, 32, 16, 8, 6, 4, 3, 2]; - println!("lumen loss-harness — 25% FEC, {frames} frames of {frame_len} bytes"); + println!("punktfunk loss-harness — 25% FEC, {frames} frames of {frame_len} bytes"); println!("(GF8 = P1/GameStream-compat, GF16 = P2/wall-breaker)\n"); println!( "{:>10} {:>9} {:>14} {:>14}",