From bfd64ce8714abffd2cd0b9b8d1fce6d385b31163 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 10 Jun 2026 13:11:59 +0000 Subject: [PATCH] =?UTF-8?q?rename:=20lumen=20=E2=86=92=20punktfunk,=20ever?= =?UTF-8?q?ywhere?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full project rename, decided 2026-06-10: - Crates/binaries: punktfunk-core / punktfunk-host / punktfunk-client-rs. - C ABI: punktfunk_* symbols, Punktfunk* types, include/punktfunk_core.h, PUNKTFUNK_FEATURE_QUIC guard (header regenerated; cbindgen renames updated, incl. PUNKTFUNK_BTN_*/PUNKTFUNK_AXIS_* wire constants). - Protocol: punktfunk/1 — control-plane magic LMN1 → PKF1, nonce salt lmn1 → pkf1. WIRE BREAK: clients must be rebuilt from this revision. - Env knobs: PUNKTFUNK_VIDEO_SOURCE / PUNKTFUNK_COMPOSITOR / PUNKTFUNK_ZEROCOPY / …. - Host config dir: ~/.config/punktfunk (the box's dir was migrated in place — the persistent identity is unchanged, pinned fingerprints stay valid). - Swift package: PunktfunkKit + PunktfunkCore.xcframework + PunktfunkConnection (Sources/PunktfunkClient app + tests renamed with it); build-xcframework.sh updated. - scripts/: 60-punktfunk.rules, punktfunk-host.service; OpenAPI doc regenerated. Also: scripts/headless/run-headless-kde.sh — full headless Plasma bringup. Root cause of "desktop but no apps/settings" over the stream: plasmashell launched without XDG_MENU_PREFIX=plasma-, so the launcher resolved a nonexistent applications.menu and rendered an empty menu. The script sets the complete KDE session env (menu prefix, KDE_FULL_SESSION, session version) and rebuilds ksycoca before starting plasmashell. Gate: 97/97 tests, clippy -D warnings (both feature sets), fmt, C-ABI harness PASS, zero lumen references left outside .git. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/ci.yml | 10 +- .gitignore | 2 +- CLAUDE.md | 79 +++-- Cargo.lock | 170 +++++------ Cargo.toml | 12 +- README.md | 38 +-- clients/android/README.md | 12 +- clients/apple/Package.swift | 22 +- clients/apple/README.md | 40 +-- .../ContentView.swift | 26 +- .../PunktfunkClientApp.swift} | 6 +- .../SessionModel.swift | 12 +- .../{LumenKit => PunktfunkKit}/AnnexB.swift | 2 +- .../InputCapture.swift | 10 +- .../PunktfunkConnection.swift} | 96 +++--- .../StreamView.swift | 16 +- .../AnnexBTests.swift | 2 +- .../LoopbackIntegrationTests.swift | 16 +- .../RemoteFirstLightTests.swift | 14 +- .../VideoToolboxRoundTripTests.swift | 4 +- clients/apple/test-loopback.sh | 10 +- crates/lumen-core/cbindgen.toml | 56 ---- .../Cargo.toml | 6 +- .../src/main.rs | 36 +-- .../{lumen-core => punktfunk-core}/Cargo.toml | 12 +- .../{lumen-core => punktfunk-core}/build.rs | 10 +- crates/punktfunk-core/cbindgen.toml | 56 ++++ .../{lumen-core => punktfunk-core}/src/abi.rs | 283 +++++++++--------- .../src/client.rs | 58 ++-- .../src/config.rs | 22 +- .../src/crypto.rs | 6 +- .../src/error.rs | 30 +- .../src/fec/gf16.rs | 0 .../src/fec/gf8.rs | 0 .../src/fec/mod.rs | 0 .../src/input.rs | 2 +- .../{lumen-core => punktfunk-core}/src/lib.rs | 10 +- .../src/packet.rs | 20 +- .../src/quic.rs | 18 +- .../src/session.rs | 18 +- .../src/stats.rs | 2 +- .../src/transport/loopback.rs | 0 .../src/transport/mod.rs | 0 .../src/transport/udp.rs | 0 .../tests/c/harness.c | 42 +-- .../tests/c/run.sh | 16 +- .../tests/c_abi.rs | 16 +- .../tests/loopback.rs | 14 +- .../vendor/fec-rs/Cargo.toml | 0 .../vendor/fec-rs/LICENSE | 0 .../vendor/fec-rs/README.md | 0 .../vendor/fec-rs/build.rs | 0 .../vendor/fec-rs/src/errors.rs | 0 .../vendor/fec-rs/src/galois.rs | 0 .../vendor/fec-rs/src/lib.rs | 0 .../vendor/fec-rs/src/matrix.rs | 0 .../vendor/fec-rs/src/reed_solomon.rs | 0 .../{lumen-host => punktfunk-host}/Cargo.toml | 8 +- .../protocols/zkde-screencast-unstable-v1.xml | 0 .../src/audio.rs | 0 .../src/audio/linux.rs | 4 +- .../src/capture.rs | 2 +- .../src/capture/linux.rs | 10 +- .../src/encode.rs | 2 +- .../src/encode/linux.rs | 6 +- .../src/gamestream/apps.rs | 4 +- .../src/gamestream/audio.rs | 6 +- .../src/gamestream/cert.rs | 4 +- .../src/gamestream/control.rs | 4 +- .../src/gamestream/crypto.rs | 2 +- .../src/gamestream/gamepad.rs | 0 .../src/gamestream/input.rs | 4 +- .../src/gamestream/mdns.rs | 0 .../src/gamestream/mod.rs | 12 +- .../src/gamestream/nvhttp.rs | 2 +- .../src/gamestream/pairing.rs | 0 .../src/gamestream/rtsp.rs | 2 +- .../src/gamestream/serverinfo.rs | 0 .../src/gamestream/stream.rs | 24 +- .../src/gamestream/tls.rs | 0 .../src/gamestream/video.rs | 4 +- .../src/inject.rs | 14 +- .../src/inject/gamepad.rs | 4 +- .../src/inject/libei.rs | 6 +- .../src/inject/wlr.rs | 4 +- .../{lumen-host => punktfunk-host}/src/m0.rs | 24 +- .../{lumen-host => punktfunk-host}/src/m3.rs | 135 +++++---- .../src/main.rs | 39 +-- .../src/mgmt.rs | 30 +- .../src/pipeline.rs | 8 +- .../src/pwinit.rs | 0 .../src/vdisplay.rs | 14 +- .../src/vdisplay/gamescope.rs | 21 +- .../src/vdisplay/kwin.rs | 4 +- .../src/vdisplay/mutter.rs | 4 +- .../src/zerocopy/cuda.rs | 0 .../src/zerocopy/egl.rs | 0 .../src/zerocopy/mod.rs | 6 +- .../src/zerocopy/vulkan.rs | 0 docs/api/openapi.json | 12 +- docs/implementation-plan.md | 62 ++-- docs/linux-setup.md | 30 +- docs/m2-plan.md | 16 +- .../gamestream-protocol-research.json | 94 +++--- include/{lumen_core.h => punktfunk_core.h} | 278 ++++++++--------- .../{60-lumen.rules => 60-punktfunk.rules} | 4 +- scripts/bootstrap-ubuntu.sh | 6 +- scripts/build-xcframework.sh | 42 +-- scripts/headless/capture-smoke-test.sh | 2 +- scripts/headless/env.sh | 2 +- scripts/headless/prepare-session.sh | 12 +- scripts/headless/run-headless-kde.sh | 41 +++ scripts/headless/run-headless-sway.sh | 4 +- scripts/headless/sway.config | 2 +- scripts/host.env.example | 16 +- scripts/lumen-host.service | 21 -- scripts/punktfunk-host.service | 21 ++ tools/loss-harness/Cargo.toml | 4 +- tools/loss-harness/src/main.rs | 16 +- 119 files changed, 1245 insertions(+), 1185 deletions(-) rename clients/apple/Sources/{LumenClient => PunktfunkClient}/ContentView.swift (83%) rename clients/apple/Sources/{LumenClient/LumenClientApp.swift => PunktfunkClient/PunktfunkClientApp.swift} (82%) rename clients/apple/Sources/{LumenClient => PunktfunkClient}/SessionModel.swift (90%) rename clients/apple/Sources/{LumenKit => PunktfunkKit}/AnnexB.swift (98%) rename clients/apple/Sources/{LumenKit => PunktfunkKit}/InputCapture.swift (97%) rename clients/apple/Sources/{LumenKit/LumenConnection.swift => PunktfunkKit/PunktfunkConnection.swift} (74%) rename clients/apple/Sources/{LumenKit => PunktfunkKit}/StreamView.swift (91%) rename clients/apple/Tests/{LumenKitTests => PunktfunkKitTests}/AnnexBTests.swift (98%) rename clients/apple/Tests/{LumenKitTests => PunktfunkKitTests}/LoopbackIntegrationTests.swift (82%) rename clients/apple/Tests/{LumenKitTests => PunktfunkKitTests}/RemoteFirstLightTests.swift (85%) rename clients/apple/Tests/{LumenKitTests => PunktfunkKitTests}/VideoToolboxRoundTripTests.swift (98%) delete mode 100644 crates/lumen-core/cbindgen.toml rename crates/{lumen-client-rs => punktfunk-client-rs}/Cargo.toml (67%) rename crates/{lumen-client-rs => punktfunk-client-rs}/src/main.rs (91%) rename crates/{lumen-core => punktfunk-core}/Cargo.toml (81%) rename crates/{lumen-core => punktfunk-core}/build.rs (70%) create mode 100644 crates/punktfunk-core/cbindgen.toml rename crates/{lumen-core => punktfunk-core}/src/abi.rs (71%) rename crates/{lumen-core => punktfunk-core}/src/client.rs (84%) rename crates/{lumen-core => punktfunk-core}/src/config.rs (92%) rename crates/{lumen-core => punktfunk-core}/src/crypto.rs (97%) rename crates/{lumen-core => punktfunk-core}/src/error.rs (56%) rename crates/{lumen-core => punktfunk-core}/src/fec/gf16.rs (100%) rename crates/{lumen-core => punktfunk-core}/src/fec/gf8.rs (100%) rename crates/{lumen-core => punktfunk-core}/src/fec/mod.rs (100%) rename crates/{lumen-core => punktfunk-core}/src/input.rs (99%) rename crates/{lumen-core => punktfunk-core}/src/lib.rs (87%) rename crates/{lumen-core => punktfunk-core}/src/packet.rs (97%) rename crates/{lumen-core => punktfunk-core}/src/quic.rs (96%) rename crates/{lumen-core => punktfunk-core}/src/session.rs (93%) rename crates/{lumen-core => punktfunk-core}/src/stats.rs (96%) rename crates/{lumen-core => punktfunk-core}/src/transport/loopback.rs (100%) rename crates/{lumen-core => punktfunk-core}/src/transport/mod.rs (100%) rename crates/{lumen-core => punktfunk-core}/src/transport/udp.rs (100%) rename crates/{lumen-core => punktfunk-core}/tests/c/harness.c (73%) rename crates/{lumen-core => punktfunk-core}/tests/c/run.sh (57%) rename crates/{lumen-core => punktfunk-core}/tests/c_abi.rs (84%) rename crates/{lumen-core => punktfunk-core}/tests/loopback.rs (94%) rename crates/{lumen-core => punktfunk-core}/vendor/fec-rs/Cargo.toml (100%) rename crates/{lumen-core => punktfunk-core}/vendor/fec-rs/LICENSE (100%) rename crates/{lumen-core => punktfunk-core}/vendor/fec-rs/README.md (100%) rename crates/{lumen-core => punktfunk-core}/vendor/fec-rs/build.rs (100%) rename crates/{lumen-core => punktfunk-core}/vendor/fec-rs/src/errors.rs (100%) rename crates/{lumen-core => punktfunk-core}/vendor/fec-rs/src/galois.rs (100%) rename crates/{lumen-core => punktfunk-core}/vendor/fec-rs/src/lib.rs (100%) rename crates/{lumen-core => punktfunk-core}/vendor/fec-rs/src/matrix.rs (100%) rename crates/{lumen-core => punktfunk-core}/vendor/fec-rs/src/reed_solomon.rs (100%) rename crates/{lumen-host => punktfunk-host}/Cargo.toml (93%) rename crates/{lumen-host => punktfunk-host}/protocols/zkde-screencast-unstable-v1.xml (100%) rename crates/{lumen-host => punktfunk-host}/src/audio.rs (100%) rename crates/{lumen-host => punktfunk-host}/src/audio/linux.rs (98%) rename crates/{lumen-host => punktfunk-host}/src/capture.rs (98%) rename crates/{lumen-host => punktfunk-host}/src/capture/linux.rs (99%) rename crates/{lumen-host => punktfunk-host}/src/encode.rs (98%) rename crates/{lumen-host => punktfunk-host}/src/encode/linux.rs (98%) rename crates/{lumen-host => punktfunk-host}/src/gamestream/apps.rs (97%) rename crates/{lumen-host => punktfunk-host}/src/gamestream/audio.rs (97%) rename crates/{lumen-host => punktfunk-host}/src/gamestream/cert.rs (97%) rename crates/{lumen-host => punktfunk-host}/src/gamestream/control.rs (99%) rename crates/{lumen-host => punktfunk-host}/src/gamestream/crypto.rs (98%) rename crates/{lumen-host => punktfunk-host}/src/gamestream/gamepad.rs (100%) rename crates/{lumen-host => punktfunk-host}/src/gamestream/input.rs (98%) rename crates/{lumen-host => punktfunk-host}/src/gamestream/mdns.rs (100%) rename crates/{lumen-host => punktfunk-host}/src/gamestream/mod.rs (96%) rename crates/{lumen-host => punktfunk-host}/src/gamestream/nvhttp.rs (99%) rename crates/{lumen-host => punktfunk-host}/src/gamestream/pairing.rs (100%) rename crates/{lumen-host => punktfunk-host}/src/gamestream/rtsp.rs (99%) rename crates/{lumen-host => punktfunk-host}/src/gamestream/serverinfo.rs (100%) rename crates/{lumen-host => punktfunk-host}/src/gamestream/stream.rs (96%) rename crates/{lumen-host => punktfunk-host}/src/gamestream/tls.rs (100%) rename crates/{lumen-host => punktfunk-host}/src/gamestream/video.rs (98%) rename crates/{lumen-host => punktfunk-host}/src/inject.rs (95%) rename crates/{lumen-host => punktfunk-host}/src/inject/gamepad.rs (98%) rename crates/{lumen-host => punktfunk-host}/src/inject/libei.rs (98%) rename crates/{lumen-host => punktfunk-host}/src/inject/wlr.rs (99%) rename crates/{lumen-host => punktfunk-host}/src/m0.rs (90%) rename crates/{lumen-host => punktfunk-host}/src/m3.rs (86%) rename crates/{lumen-host => punktfunk-host}/src/main.rs (89%) rename crates/{lumen-host => punktfunk-host}/src/mgmt.rs (97%) rename crates/{lumen-host => punktfunk-host}/src/pipeline.rs (84%) rename crates/{lumen-host => punktfunk-host}/src/pwinit.rs (100%) rename crates/{lumen-host => punktfunk-host}/src/vdisplay.rs (92%) rename crates/{lumen-host => punktfunk-host}/src/vdisplay/gamescope.rs (90%) rename crates/{lumen-host => punktfunk-host}/src/vdisplay/kwin.rs (99%) rename crates/{lumen-host => punktfunk-host}/src/vdisplay/mutter.rs (98%) rename crates/{lumen-host => punktfunk-host}/src/zerocopy/cuda.rs (100%) rename crates/{lumen-host => punktfunk-host}/src/zerocopy/egl.rs (100%) rename crates/{lumen-host => punktfunk-host}/src/zerocopy/mod.rs (90%) rename crates/{lumen-host => punktfunk-host}/src/zerocopy/vulkan.rs (100%) rename include/{lumen_core.h => punktfunk_core.h} (53%) rename scripts/{60-lumen.rules => 60-punktfunk.rules} (76%) create mode 100755 scripts/headless/run-headless-kde.sh delete mode 100644 scripts/lumen-host.service create mode 100644 scripts/punktfunk-host.service diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index f31d676..d3ae1fb 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 5a465ed..904f3ae 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 aae50fd..1c1fa67 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 fde1b66..ca874b0 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 fc3682d..fbb8d1d 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 df39fc3..770cdc6 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 49dc408..ec6a31b 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 3fa0316..52125ba 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 3325d78..09c8960 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 58e2670..ae448b5 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 42f2481..308c8ab 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 598fe62..aa9942f 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 00a5414..ed9436d 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 5685e0a..54deb90 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 7dc6086..820130c 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 40bb87f..c112e43 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 e4a32ed..8bde705 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 d7a0b13..21f4baa 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 afd094e..aba9b2b 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 e9ab429..03c3b46 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 3c21896..16a0ec0 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 ea52896..0000000 --- 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 c782de1..a7f8053 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 90ea18f..856ad20 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 dc2bb39..023228b 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 f0a8551..134c8c8 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 0000000..c316666 --- /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 94fe555..de38949 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 3997986..2c329e2 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 ed303a5..feae7ef 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 2917096..2ec5d63 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 f383c10..e299e7d 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 d1e7434..84a38ca 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 2ba9240..090b351 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 0bad48a..8d5b5b8 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 e3df367..0c90f05 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 ab4bb97..e26a8d0 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 5b6974b..74a798c 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 c6e8c24..37e0476 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 1bfb0e7..dd0b368 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 da3a06c..b35f353 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 f7a958f..d4aeb10 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 8607413..84bb485 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 6de725f..e908e74 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 185321d..ed07a58 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 07c4c4a..9c2ae75 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 af3c2e8..8b1015c 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 564d55f..384be78 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 1755bda..7cc6f27 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 27fac9c..b408562 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 7ade341..94d5a3d 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 489e157..7214142 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 23791f5..6f91c01 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 83470c0..0e554cd 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 b630bdf..3094b59 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 72b3979..f172998 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 65f2e46..8808c4f 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 239f022..9b039b9 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 a1d4488..70ce517 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 ef0d685..0ed0576 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 f23305f..49f38cb 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 fd315ab..0ca5b90 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 96c3666..e28c596 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 6682b0a..3c46bbd 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 b35e350..7a92ec6 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 7d14f13..f812888 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 0407d86..1c67319 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 3e3ce20..71f338a 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 028158d..d74fb62 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 cf940f7..e5f7043 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 b725110..8d2a85e 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 f6473ac..c899f17 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 b8d9aed..0f6a81f 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/docs/api/openapi.json b/docs/api/openapi.json index 7568cc0..7306135 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -1,8 +1,8 @@ { "openapi": "3.1.0", "info": { - "title": "lumen management API", - "description": "Control-plane API for managing a lumen 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 management token (mandatory for non-loopback binds).", + "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 management token (mandatory for non-loopback binds).", "contact": { "name": "unom" }, @@ -393,7 +393,7 @@ "abi_version": { "type": "integer", "format": "int32", - "description": "`lumen-core` C ABI version.", + "description": "`punktfunk-core` C ABI version.", "minimum": 0 }, "status": { @@ -403,7 +403,7 @@ }, "version": { "type": "string", - "description": "`lumen-host` crate version." + "description": "`punktfunk-host` crate version." } } }, @@ -425,7 +425,7 @@ "abi_version": { "type": "integer", "format": "int32", - "description": "`lumen-core` C ABI version.", + "description": "`punktfunk-core` C ABI version.", "minimum": 0 }, "app_version": { @@ -459,7 +459,7 @@ }, "version": { "type": "string", - "description": "`lumen-host` crate version." + "description": "`punktfunk-host` crate version." } } }, diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index cab27d5..f8df270 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -1,8 +1,8 @@ -# lumen — Implementation Plan +# punktfunk — Implementation Plan *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 — rename freely. It fits the lowercase house style (`unom`, `played`, `remplir`) and reads as "glass-to-glass light," which is the whole point. +> `punktfunk` is a placeholder codename — rename freely. It fits the lowercase house style (`unom`, `played`, `remplir`) and reads as "glass-to-glass light," which is the whole point. --- @@ -45,14 +45,14 @@ flowchart TD VD --> CAP --> ENC ENC --> COREH IN_H["Input injector
(libei / uinput)"] - COREH["lumen-core (C ABI)
protocol · FEC · pacing · crypto"] + COREH["punktfunk-core (C ABI)
protocol · FEC · pacing · crypto"] COREH --> IN_H end COREH <-->|"UDP+FEC video / QUIC control+audio"| COREC subgraph Client["Client (Rust / Swift / Kotlin)"] - COREC["lumen-core (same crate, C ABI)"] + COREC["punktfunk-core (same crate, C ABI)"] DEC["Decoder
(VideoToolbox / NVDEC / VAAPI)"] PRES["Present + frame pacing"] INP["Input capture"] @@ -61,7 +61,7 @@ flowchart TD end ``` -**The load-bearing decision:** `lumen-core` is one crate, compiled once, linked by every host and client through a C ABI. Protocol logic, FEC, packet pacing, jitter buffering, pairing, and crypto live there and exist exactly once. Platform code (capture, encode, decode, present, input, UI) lives outside the core and is written in whatever language suits the platform. +**The load-bearing decision:** `punktfunk-core` is one crate, compiled once, linked by every host and client through a C ABI. Protocol logic, FEC, packet pacing, jitter buffering, pairing, and crypto live there and exist exactly once. Platform code (capture, encode, decode, present, input, UI) lives outside the core and is written in whatever language suits the platform. --- @@ -70,8 +70,8 @@ flowchart TD | Phase | Protocol | Clients that work | Bitrate ceiling | Purpose | |------|----------|-------------------|-----------------|---------| | **P1** | GameStream-compatible (existing Moonlight wire format) | All existing Moonlight/Artemis clients | ~1 Gbps (legacy GF(2⁸) FEC) | Ship the Linux virtual-display win with zero client work | -| **P2** | `lumen/1` negotiated extension: GF(2¹⁶) FEC, multi-block framing, optional QUIC control | lumen clients only; falls back to P1 for others | Multi-Gbps | Break the wall; introduce native clients | -| **P3** | `lumen/1` as primary; GameStream kept as compat shim | lumen everywhere, Moonlight as fallback | Multi-Gbps | Full control of features (mic passthrough, per-client identity, HDR signalling) | +| **P2** | `punktfunk/1` negotiated extension: GF(2¹⁶) FEC, multi-block framing, optional QUIC control | punktfunk clients only; falls back to P1 for others | Multi-Gbps | Break the wall; introduce native clients | +| **P3** | `punktfunk/1` as primary; GameStream kept as compat shim | punktfunk everywhere, Moonlight as fallback | Multi-Gbps | Full control of features (mic passthrough, per-client identity, HDR signalling) | Negotiation: extend the `serverinfo`/RTSP `SETUP` handshake with a capability flag. Old clients never see the flag and get P1 behavior. This is how Apollo/Artemis diverge cleanly, and it keeps you compatible while you build. @@ -91,7 +91,7 @@ Negotiation: extend the `serverinfo`/RTSP `SETUP` handshake with a capability fl | QUIC (control/audio) | `quinn` | Datagram ext for audio; reliable streams for control | | TLS / crypto | `rustls` + `ring` (or `aws-lc-rs`) | Pairing, session keys (AES-GCM to match GameStream in P1) | | Serialization | `zerocopy` / `bytes` | Wire structs `#[repr(C)]`, zero-copy parse | -| C header gen | `cbindgen` | Generates `lumen_core.h` from the ABI module | +| C header gen | `cbindgen` | Generates `punktfunk_core.h` from the ABI module | | Error/log | `tracing` | Structured; feature-gate off the hot path | ### Linux host dependencies @@ -107,7 +107,7 @@ Negotiation: extend the `serverinfo`/RTSP `SETUP` handshake with a capability fl ### Apple client (P2+) -Swift + VideoToolbox (decode) + Metal (present) + SwiftUI. Imports `lumen_core.h` directly via a module map — no glue layer. +Swift + VideoToolbox (decode) + Metal (present) + SwiftUI. Imports `punktfunk_core.h` directly via a module map — no glue layer. ### Ruled out @@ -123,32 +123,32 @@ Swift + VideoToolbox (decode) + Metal (present) + SwiftUI. Imports `lumen_core.h Design it on day one; retrofitting an ABI is painful. **Principles** -- Opaque handles only across the boundary: `LumenSession*`, never Rust types. +- Opaque handles only across the boundary: `PunktfunkSession*`, never Rust types. - All cross-boundary structs are `#[repr(C)]`; primitives + pointer/len pairs for buffers. - Async events via registered C callbacks (`fn ptr` + `void* userdata`). -- Explicit, documented ownership: who frees what, when. Provide `lumen_*_free` for every allocation that crosses out. -- Versioned ABI: `uint32_t lumen_abi_version(void)` + a `LumenConfig` struct whose first field is its own size for forward-compat. +- Explicit, documented ownership: who frees what, when. Provide `punktfunk_*_free` for every allocation that crosses out. +- Versioned ABI: `uint32_t punktfunk_abi_version(void)` + a `PunktfunkConfig` struct whose first field is its own size for forward-compat. **Minimal surface (sketch)** ```c // lifecycle -LumenSession* lumen_session_new(const LumenConfig* cfg); -void lumen_session_free(LumenSession*); +PunktfunkSession* punktfunk_session_new(const PunktfunkConfig* cfg); +void punktfunk_session_free(PunktfunkSession*); // host: feed an encoded access unit (the core does FEC + packetize + pace + send) -int lumen_host_submit_frame(LumenSession*, const uint8_t* data, size_t len, - uint64_t pts_ns, LumenFrameFlags flags); +int punktfunk_host_submit_frame(PunktfunkSession*, const uint8_t* data, size_t len, + uint64_t pts_ns, PunktfunkFrameFlags flags); // client: pull a reassembled, FEC-recovered access unit ready to decode -int lumen_client_poll_frame(LumenSession*, LumenFrame* out /*borrowed until next poll*/); +int punktfunk_client_poll_frame(PunktfunkSession*, PunktfunkFrame* out /*borrowed until next poll*/); // input (both directions): client captures, host receives via callback -int lumen_send_input(LumenSession*, const LumenInputEvent*); -void lumen_set_input_callback(LumenSession*, LumenInputCb, void* user); +int punktfunk_send_input(PunktfunkSession*, const PunktfunkInputEvent*); +void punktfunk_set_input_callback(PunktfunkSession*, PunktfunkInputCb, void* user); // stats for the frame-pacing/quality logic and the web UI -void lumen_get_stats(LumenSession*, LumenStats* out); +void punktfunk_get_stats(PunktfunkSession*, PunktfunkStats* out); ``` Keep it this small. Everything platform-specific (how you got the encoded bytes, how you decode them) stays on the platform side. @@ -215,15 +215,15 @@ Sizing is rough and relative (Spike / S / M / L) for a focused solo dev; treat a **M0 — Pipeline spike (S).** wlroots headless output → PipeWire capture → VAAPI/NVENC encode → dump H.265 to a file that plays. *Acceptance:* a valid encoded file from a virtual output, no streaming yet. Proves the Linux capture+encode chain end-to-end. -**M1 — `lumen-core` skeleton + C ABI (M).** Session lifecycle, GameStream-compatible packetization and GF(2⁸) FEC (P1), AES-GCM, `cbindgen` header, a tiny C test harness. *Acceptance:* core links from C; round-trips packets in a loopback test with simulated loss. +**M1 — `punktfunk-core` skeleton + C ABI (M).** Session lifecycle, GameStream-compatible packetization and GF(2⁸) FEC (P1), AES-GCM, `cbindgen` header, a tiny C test harness. *Acceptance:* core links from C; round-trips packets in a loopback test with simulated loss. **M2 — P1 host: stream to stock Moonlight (L).** Wire M0's pipeline into the core; implement `serverinfo`/pairing/RTSP enough for a real Moonlight client to connect, with a KWin virtual output created on connect and destroyed on disconnect. Input via `reis`/uinput. *Acceptance:* **you play a game on your KDE box streamed to a stock Moonlight client on a virtual display, no dummy plug, no kernel args.** This is the shippable milestone and the project's reason to exist. **M3 — Measurement harness (S).** Glass-to-glass latency measurement (on-screen QR/timestamp or photodiode), packet-loss injection, frame-pacing and stall metrics surfaced in the web UI. *Acceptance:* you can quantify a regression. Build this before optimizing anything. -**M4 — P2 transport: break the wall (L).** Add `lumen/1` negotiation; swap to `reed-solomon-simd` GF(2¹⁶) with multi-block per-frame framing; optional QUIC control/audio. Write a minimal **Rust** reference client (decode via VAAPI, present via wgpu/Vulkan) to exercise it. *Acceptance:* a stable stream above 1.4 Gbps at 5120×1440@240 with loss recovery working; latency unchanged vs. M2. +**M4 — P2 transport: break the wall (L).** Add `punktfunk/1` negotiation; swap to `reed-solomon-simd` GF(2¹⁶) with multi-block per-frame framing; optional QUIC control/audio. Write a minimal **Rust** reference client (decode via VAAPI, present via wgpu/Vulkan) to exercise it. *Acceptance:* a stable stream above 1.4 Gbps at 5120×1440@240 with loss recovery working; latency unchanged vs. M2. -**M5 — Apple client (L).** Swift + VideoToolbox + Metal + SwiftUI, linking `lumen-core` via the C header. *Acceptance:* the Mac Studio plays a stream at native resolution/refresh. +**M5 — Apple client (L).** Swift + VideoToolbox + Metal + SwiftUI, linking `punktfunk-core` via the C header. *Acceptance:* the Mac Studio plays a stream at native resolution/refresh. **M6 — Feature surface (M, ongoing).** Mic passthrough as a proper encrypted, per-client reverse audio stream (the thing the upstream PR got wrong); HDR signalling; per-client identity/permissions; pause/resume. *Acceptance:* feature parity with Apollo on the items you care about, plus mic done right. @@ -257,26 +257,26 @@ Sizing is rough and relative (Spike / S / M / L) for a focused solo dev; treat a ## 11. Repo / workspace structure ``` -lumen/ +punktfunk/ ├── Cargo.toml # workspace ├── crates/ -│ ├── lumen-core/ # protocol, FEC, pacing, crypto — C ABI (cdylib + staticlib) +│ ├── punktfunk-core/ # protocol, FEC, pacing, crypto — C ABI (cdylib + staticlib) │ │ ├── src/abi.rs # #[no_mangle] extern "C" surface │ │ ├── src/fec.rs # GF(2^16) blocking over reed-solomon-simd │ │ ├── src/transport/ # udp+fec video, quinn control/audio -│ │ ├── src/protocol/ # gamestream-compat (P1) + lumen/1 (P2) +│ │ ├── src/protocol/ # gamestream-compat (P1) + punktfunk/1 (P2) │ │ └── cbindgen.toml -│ ├── lumen-host/ # Linux host binary +│ ├── punktfunk-host/ # Linux host binary │ │ ├── src/capture/ # pipewire / portal │ │ ├── src/encode/ # ffmpeg vaapi/nvenc │ │ ├── src/vdisplay/ # trait + kwin/wlroots/mutter impls │ │ ├── src/input/ # reis + uinput │ │ └── src/web/ # axum config/pairing API -│ └── lumen-client-rs/ # reference Rust client (M4) +│ └── punktfunk-client-rs/ # reference Rust client (M4) ├── clients/ -│ ├── apple/ # Swift package, imports lumen_core.h (M5) +│ ├── apple/ # Swift package, imports punktfunk_core.h (M5) │ └── android/ # Kotlin + JNI (later) -├── include/ # generated lumen_core.h +├── include/ # generated punktfunk_core.h └── tools/ ├── latency-probe/ └── loss-harness/ @@ -286,7 +286,7 @@ lumen/ ## 12. Immediate next actions (first week) -1. **Stand up the workspace** with `lumen-core` (empty ABI + `cbindgen`) and `lumen-host` skeletons; CI on your Gitea (you already have BuildKit pipelines). +1. **Stand up the workspace** with `punktfunk-core` (empty ABI + `cbindgen`) and `punktfunk-host` skeletons; CI on your Gitea (you already have BuildKit pipelines). 2. **M0 spike on wlroots:** headless output → PipeWire capture → NVENC/VAAPI encode → playable file. This validates the riskiest *pipeline* assumptions in days, on your real GPU. 3. **Read KRdp's source** for how KDE creates virtual outputs and casts them — it's the closest existing reference for the KWin path you'll need in M2. 4. **Decide P1 protocol depth:** confirm exactly which `serverinfo`/RTSP/pairing messages a current Moonlight client requires for a successful connect, so M2's compat surface is scoped precisely (this is also the question to take back to the dev who mentioned the 1G limit). diff --git a/docs/linux-setup.md b/docs/linux-setup.md index 2428815..780d72c 100644 --- a/docs/linux-setup.md +++ b/docs/linux-setup.md @@ -1,8 +1,8 @@ # Linux host setup — NVIDIA GPU VM (M0/M2) -How to bring up the build environment for the lumen Linux host on an NVIDIA-GPU Ubuntu VM -and run the **M0** capture→encode spike. `lumen-core` already builds and is tested -cross-platform; this is about the platform backends in `crates/lumen-host`. +How to bring up the build environment for the punktfunk Linux host on an NVIDIA-GPU Ubuntu VM +and run the **M0** capture→encode spike. `punktfunk-core` already builds and is tested +cross-platform; this is about the platform backends in `crates/punktfunk-host`. > Target **Ubuntu 24.04 (noble)**: Sway 1.9, FFmpeg 6.1.1, xdg-desktop-portal 1.18. > 22.04 (jammy) ships Sway 1.7 / FFmpeg 4.4 — too old for this path; build from source or @@ -11,7 +11,7 @@ cross-platform; this is about the platform backends in `crates/lumen-host`. ## 1. Bootstrap ```sh -git clone git@git.unom.io:unom/lumen.git && cd lumen && git checkout m1-lumen-core +git clone git@git.unom.io:unom/punktfunk.git && cd punktfunk && git checkout m1-punktfunk-core bash scripts/bootstrap-ubuntu.sh ``` @@ -68,11 +68,11 @@ bash scripts/headless/run-headless-sway.sh # success logs "EGL vendor: NV # shell 2 — same user: set the client mode, import the portal env, write the env file bash scripts/headless/prepare-session.sh 2560x1440@60Hz -source /tmp/lumen-sway-env.sh +source /tmp/punktfunk-sway-env.sh swaymsg -t get_outputs # confirm HEADLESS-1 active swaymsg exec foot # optional: animated content to capture bash scripts/headless/capture-smoke-test.sh # wf-recorder (wlr-screencopy) -> hevc_nvenc -ffprobe /tmp/lumen-headless-test.mkv # confirm a real H.265 stream +ffprobe /tmp/punktfunk-headless-test.mkv # confirm a real H.265 stream ``` `wf-recorder` uses `wlr-screencopy` directly (no portal/D-Bus) — the fastest way to @@ -89,26 +89,26 @@ The wlroots-on-NVIDIA env workarounds (`WLR_RENDERER=gles2`, `WLR_NO_HARDWARE_CU `GBM_BACKEND=nvidia-drm`, `sway --unsupported-gpu`, …) live in `scripts/headless/env.sh` — `source` it before launching anything Wayland. -## 4. M0 proper — wire it into `lumen-core` +## 4. M0 proper — wire it into `punktfunk-core` Goal (plan §8): headless output → PipeWire ScreenCast → NVENC → a playable file, then feed -the encoded access units into a `lumen_core::Session` (host role). The module seams exist -in `crates/lumen-host/src/{vdisplay,capture,encode,inject,pipeline}.rs`. +the encoded access units into a `punktfunk_core::Session` (host role). The module seams exist +in `crates/punktfunk-host/src/{vdisplay,capture,encode,inject,pipeline}.rs`. -**Status: implemented and verified end-to-end** in `crates/lumen-host` (`m0.rs`, +**Status: implemented and verified end-to-end** in `crates/punktfunk-host` (`m0.rs`, `capture/linux.rs`, `encode/linux.rs`). After the §3 bring-up: ```sh -source /tmp/lumen-sway-env.sh +source /tmp/punktfunk-sway-env.sh swaymsg exec foot # animated content # Live portal capture → NVENC HEVC → playable file, with each AU also round-tripped -# through a lumen_core host→client Session (FEC + packetize + reassemble) and verified: -cargo run -p lumen-host -- m0 --source portal --seconds 5 --out /tmp/lumen-m0.h265 -ffprobe /tmp/lumen-m0.h265 +# through a punktfunk_core host→client Session (FEC + packetize + reassemble) and verified: +cargo run -p punktfunk-host -- m0 --source portal --seconds 5 --out /tmp/punktfunk-m0.h265 +ffprobe /tmp/punktfunk-m0.h265 # No capture session needed (encode + core only): --source synthetic ``` -Verified result: `1920x1080` HEVC, ~300 frames in 5s, `lumen-core loopback … 0 mismatches`. +Verified result: `1920x1080` HEVC, ~300 frames in 5s, `punktfunk-core loopback … 0 mismatches`. The portal negotiates packed **`RGB` (24-bit, 3 bpp)** on wlroots; the encoder expands it to `rgb0` (one pad byte/pixel, no colour math) since NVENC accepts `rgb0`/`bgr0` but not `rgb24`. dmabuf zero-copy import is still deferred (plan §9) — this is the CPU-copy path. diff --git a/docs/m2-plan.md b/docs/m2-plan.md index 1afcde6..67b3c2c 100644 --- a/docs/m2-plan.md +++ b/docs/m2-plan.md @@ -7,11 +7,11 @@ Ground-truth protocol reference: [`research/gamestream-protocol-research.json`]( ## Architecture (respects the "one core" invariant) -- **lumen-core** gains a **P1 GameStream wire codec** (`ProtocolPhase::P1GameStream`, the +- **punktfunk-core** gains a **P1 GameStream wire codec** (`ProtocolPhase::P1GameStream`, the hook already exists): the exact RTP+`NV_VIDEO_PACKET` framing, the GameStream FEC shard layout, and the video/audio AES-GCM/CBC paths. Hot path, native threads, **no async**. - Kept beside lumen's native internal format (P2), selected by phase. -- **lumen-host** gains the **control plane** (tokio/axum OK — I/O-bound, not the hot path): + Kept beside punktfunk's native internal format (P2), selected by phase. +- **punktfunk-host** gains the **control plane** (tokio/axum OK — I/O-bound, not the hot path): mDNS discovery, nvhttp serverinfo + the 4-phase pairing, the RTSP handshake, the ENet control stream + input injection, the virtual-display lifecycle, and Opus audio encode. @@ -46,16 +46,16 @@ Ground-truth protocol reference: [`research/gamestream-protocol-research.json`]( the client cert is pinned for subsequent HTTPS. 4 phases over `/pair?phrase=…`. - **RTSP** `Session: DEADBEEFCAFE;timeout = 90` (literal), `Transport: server_port=

`, `streamid=video/0/0` / `control/13/0`. ANNOUNCE carries the negotiated config - (`x-nv-video[0].*`, `x-nv-vqos[0].*`) → maps to `lumen_core::Config`. + (`x-nv-video[0].*`, `x-nv-vqos[0].*`) → maps to `punktfunk_core::Config`. ## The two highest interop risks (validate EARLY) 1. **RS-FEC matrix compatibility.** Sunshine + Moonlight both use **nanors** (GF(2⁸), poly - 0x11d, Vandermonde systematic). lumen-core uses `reed-solomon-erasure` (Cauchy) — parity + 0x11d, Vandermonde systematic). punktfunk-core uses `reed-solomon-erasure` (Cauchy) — parity bytes likely **don't match**, so Moonlight silently fails to recover any frame with a lost data shard. Mitigation: **on a clean LAN with no loss the client never runs RS decode**, so defer this — get a frame decoded first, then FFI/port nanors for loss recovery. -2. **Crypto layout.** lumen's `SessionCrypto` (salt + seq-as-AAD) is wire-incompatible. P1 +2. **Crypto layout.** punktfunk's `SessionCrypto` (salt + seq-as-AAD) is wire-incompatible. P1 needs a separate GameStream GCM path. Mitigation: **video encryption is negotiated and usually off on LAN** — implement plaintext video first, add GCM later. @@ -67,8 +67,8 @@ Ground-truth protocol reference: [`research/gamestream-protocol-research.json`]( - **P1.2 — Launch + RTSP + virtual display.** `/launch` (parse rikey/rikeyid/mode), the RTSP handshake, negotiate `Config`, create a wlroots virtual output sized to the client. *Acceptance: Moonlight completes RTSP and the host stands up the UDP streams.* -- **P1.3 — Video (lumen-core P1 codec), plaintext, clean-LAN.** RTP+NV framing + FEC shard - layout in lumen-core; wire M0's NVENC AUs → UDP 47998. *Acceptance: Moonlight DISPLAYS video.* +- **P1.3 — Video (punktfunk-core P1 codec), plaintext, clean-LAN.** RTP+NV framing + FEC shard + layout in punktfunk-core; wire M0's NVENC AUs → UDP 47998. *Acceptance: Moonlight DISPLAYS video.* - **P1.4 — Control + input.** ENet (`rusty_enet`) control stream; decode input → `inject.rs` (uinput/reis); request-IDR → force NVENC keyframe. *Acceptance: mouse/keyboard work.* - **P1.5 — Robustness: FEC recovery + encryption.** nanors-exact FEC; per-shard AES-GCM. diff --git a/docs/research/gamestream-protocol-research.json b/docs/research/gamestream-protocol-research.json index 4753674..630cf3d 100644 --- a/docs/research/gamestream-protocol-research.json +++ b/docs/research/gamestream-protocol-research.json @@ -65,9 +65,9 @@ "9. Client: GET https://host:47984/pair?...&phrase=pairchallenge over mutual-TLS presenting its now-trusted cert; expects paired=1. Pairing complete -> PairState.PAIRED.", "10. On any mismatch the client GET /unpair and returns FAILED / PIN_WRONG." ], - "crypto": "PIN-derived key: aesKey = HASH(salt[16] || PIN_utf8)[0..16], where HASH = SHA-256 if server appversion major >=7 (Sunshine: 7.1.431.-1) else SHA-1. Salt = client random 16 bytes. PIN is the 4-digit code shown/entered by user; concatenation is salt FIRST then pin. Pairing cipher: AES-128 in ECB mode, NO padding / padding DISABLED (Sunshine ecb_t(key,false); Moonlight AESLightEngine block loop). Inputs are zero-extended to a 16-byte multiple before encryption (a 32-byte SHA-256 hash = 2 blocks; respHash(32)+serverChallenge(16)=48=3 blocks). Per-side proofs: serverHash = SHA(clientChallenge || serverCert.signature || serversecret16); clientHash = SHA(serverChallenge || clientCert.signature || clientSecret16) (cert.signature = the DER signature bytes of the self-signed X.509). Identity binding: each side RSA-signs its own 16-byte secret with its cert's private key using RSA-PKCS1 over SHA-256 (sign256/verify256), other side verifies. Certs: self-signed RSA-2048, SHA-256 signed, ~20-year validity. Result of pairing is NOT a streaming key — it only establishes mutual TLS trust (pinned certs). The actual AES-128 STREAM key is delivered separately at /launch as rikey (16-byte hex) + rikeyid; that is where lumen-core's existing AES-128-GCM session crypto plugs in. IMPORTANT: pairing AES-ECB-no-pad is distinct from and unrelated to lumen-core's AES-128-GCM session sealing.", - "rust_options": "HTTP/HTTPS control plane belongs in crates/lumen-host/src/web.rs (the existing stub explicitly permits tokio/axum here, off the hot path). Use axum or hyper for the two servers. TLS with mutual auth + custom cert pinning: use rustls via axum-server/tokio-rustls with a custom ClientCertVerifier (rustls::server::danger::ClientCertVerifier) that accepts any well-formed cert at handshake time and then matches the presented leaf DER/PEM against the paired allow-list (mirror Sunshine's verify callback), OR use openssl/openssl crate to match Sunshine 1:1. XML: build/parse with quick-xml or xml-rs (or just format! the small fixed templates and a tiny extractor). Crypto: aes crate (already a dep transitively) in ECB mode via the `ecb` crate with NoPadding (Aes128 + ecb::Decryptor/Encryptor, manual block handling) — note RustCrypto deprecates ECB so call the block cipher directly (aes::Aes128 + cipher::BlockEncrypt/BlockDecrypt over 16-byte chunks). Hashing: sha2 (SHA-256) and sha1 crates. X.509 self-signed cert generation + RSA-SHA256 sign/verify: rcgen for cert gen, rsa + sha2 for PKCS1v15 sign/verify, x509-parser or x509-cert to extract the cert's signature bytes (cert.getSignature() equivalent) and to do TLS-trust comparison. Persist authorized client certs (PEM) in a small JSON/sled store. Run all of this on tokio in web.rs; keep it fully separate from the native-thread per-frame pipeline.", - "reuse_from_lumen": "REUSE little of lumen-core's crypto here — its crypto.rs is AES-128-GCM session sealing (nonce = salt||seq, seq as AAD) for the VIDEO/INPUT plane, which corresponds to the post-/launch rikey stream key, NOT the pairing handshake. Pairing needs AES-128-ECB-no-pad + SHA-256/SHA-1 + RSA, none of which exist in lumen yet and must be newly built (best placed in lumen-host, not lumen-core, since it is control-plane only). The natural seam is the existing web.rs stub (WebConfig::run) whose TODO already says 'GameStream serverinfo, pairing handshake, RTSP SETUP' — implement the two HTTP servers and the 4-phase /pair state machine there. lumen-core's AES-128-GCM SessionCrypto IS reusable downstream: once paired and /launch hands over rikey (16-byte AES key) + rikeyid, feed that key into lumen-core's Session/SessionCrypto for the encrypted video/control planes. The internal 40-byte packet format is unrelated to this HTTP/pairing area. So: new pairing crypto + axum servers in lumen-host/web.rs (control), reuse lumen-core GCM for the data plane post-launch.", + "crypto": "PIN-derived key: aesKey = HASH(salt[16] || PIN_utf8)[0..16], where HASH = SHA-256 if server appversion major >=7 (Sunshine: 7.1.431.-1) else SHA-1. Salt = client random 16 bytes. PIN is the 4-digit code shown/entered by user; concatenation is salt FIRST then pin. Pairing cipher: AES-128 in ECB mode, NO padding / padding DISABLED (Sunshine ecb_t(key,false); Moonlight AESLightEngine block loop). Inputs are zero-extended to a 16-byte multiple before encryption (a 32-byte SHA-256 hash = 2 blocks; respHash(32)+serverChallenge(16)=48=3 blocks). Per-side proofs: serverHash = SHA(clientChallenge || serverCert.signature || serversecret16); clientHash = SHA(serverChallenge || clientCert.signature || clientSecret16) (cert.signature = the DER signature bytes of the self-signed X.509). Identity binding: each side RSA-signs its own 16-byte secret with its cert's private key using RSA-PKCS1 over SHA-256 (sign256/verify256), other side verifies. Certs: self-signed RSA-2048, SHA-256 signed, ~20-year validity. Result of pairing is NOT a streaming key — it only establishes mutual TLS trust (pinned certs). The actual AES-128 STREAM key is delivered separately at /launch as rikey (16-byte hex) + rikeyid; that is where punktfunk-core's existing AES-128-GCM session crypto plugs in. IMPORTANT: pairing AES-ECB-no-pad is distinct from and unrelated to punktfunk-core's AES-128-GCM session sealing.", + "rust_options": "HTTP/HTTPS control plane belongs in crates/punktfunk-host/src/web.rs (the existing stub explicitly permits tokio/axum here, off the hot path). Use axum or hyper for the two servers. TLS with mutual auth + custom cert pinning: use rustls via axum-server/tokio-rustls with a custom ClientCertVerifier (rustls::server::danger::ClientCertVerifier) that accepts any well-formed cert at handshake time and then matches the presented leaf DER/PEM against the paired allow-list (mirror Sunshine's verify callback), OR use openssl/openssl crate to match Sunshine 1:1. XML: build/parse with quick-xml or xml-rs (or just format! the small fixed templates and a tiny extractor). Crypto: aes crate (already a dep transitively) in ECB mode via the `ecb` crate with NoPadding (Aes128 + ecb::Decryptor/Encryptor, manual block handling) — note RustCrypto deprecates ECB so call the block cipher directly (aes::Aes128 + cipher::BlockEncrypt/BlockDecrypt over 16-byte chunks). Hashing: sha2 (SHA-256) and sha1 crates. X.509 self-signed cert generation + RSA-SHA256 sign/verify: rcgen for cert gen, rsa + sha2 for PKCS1v15 sign/verify, x509-parser or x509-cert to extract the cert's signature bytes (cert.getSignature() equivalent) and to do TLS-trust comparison. Persist authorized client certs (PEM) in a small JSON/sled store. Run all of this on tokio in web.rs; keep it fully separate from the native-thread per-frame pipeline.", + "reuse_from_punktfunk": "REUSE little of punktfunk-core's crypto here — its crypto.rs is AES-128-GCM session sealing (nonce = salt||seq, seq as AAD) for the VIDEO/INPUT plane, which corresponds to the post-/launch rikey stream key, NOT the pairing handshake. Pairing needs AES-128-ECB-no-pad + SHA-256/SHA-1 + RSA, none of which exist in punktfunk yet and must be newly built (best placed in punktfunk-host, not punktfunk-core, since it is control-plane only). The natural seam is the existing web.rs stub (WebConfig::run) whose TODO already says 'GameStream serverinfo, pairing handshake, RTSP SETUP' — implement the two HTTP servers and the 4-phase /pair state machine there. punktfunk-core's AES-128-GCM SessionCrypto IS reusable downstream: once paired and /launch hands over rikey (16-byte AES key) + rikeyid, feed that key into punktfunk-core's Session/SessionCrypto for the encrypted video/control planes. The internal 40-byte packet format is unrelated to this HTTP/pairing area. So: new pairing crypto + axum servers in punktfunk-host/web.rs (control), reuse punktfunk-core GCM for the data plane post-launch.", "gotchas": [ "appversion MAJOR number is load-bearing: it silently switches the client's pairing hash. Advertise major >=7 (e.g. \"7.1.431.-1\") to get SHA-256; advertise <7 and the client uses SHA-1 with 20-byte hashes (changes all the ECB block counts). Mismatch => silent pairing failure.", "AES-ECB has NO padding and NO IV. Do not use a library that auto-pads (PKCS7) — Sunshine passes ecb_t(key,false). Inputs are zero-extended to 16-byte multiples; a 32-byte SHA-256 hash is exactly 2 blocks but respHash(32)+serverChallenge(16)=48 must be encrypted as one 48-byte buffer.", @@ -90,7 +90,7 @@ "moonlight-android PairingManager.java: serverMajorVersion>=7 -> Sha256PairingHash else Sha1PairingHash; saltPin (salt then pin utf-8); generateAesKey = copyOf(hash,16); encryptAes/decryptAes AESLightEngine ECB; performBlockCipher 16-byte block loop with zero-pad blockRoundedSize; phase byte orders (clientchallenge=enc(random16); challengeRespHash=hash(serverChallenge||cert.signature||clientSecret); clientPairingSecret=clientSecret||signData(clientSecret)); Sha256PairingHash.getHashLength=32 / Sha1=20; signData=SHA256withRSA; verifySignature; PairState{NOT_PAIRED,PAIRED,PIN_WRONG,FAILED,ALREADY_IN_PROGRESS}; executePairingChallenge final pairchallenge", "moonlight-common-c src/Limelight.h: SCM_H264=0x1, SCM_HEVC=0x100, SCM_HEVC_MAIN10=0x200, SCM_AV1_MAIN8=0x10000, SCM_AV1_MAIN10=0x20000, SCM_*_444 flags; VIDEO_FORMAT_* constants; default ports 47984/47989/48010 tcp, 47998/47999/48000/48010 udp", "DeepWiki LizardByte/Sunshine NVHTTP page: ServerCodecModeSupport decimal values 3 / 259 / 3843", - "lumen repo (local): crates/lumen-host/src/web.rs (WebConfig stub, control-plane seam) and crates/lumen-core/src/crypto.rs (AES-128-GCM SessionCrypto = data-plane, not pairing)" + "punktfunk repo (local): crates/punktfunk-host/src/web.rs (WebConfig stub, control-plane seam) and crates/punktfunk-core/src/crypto.rs (AES-128-GCM SessionCrypto = data-plane, not pairing)" ] }, { @@ -153,10 +153,10 @@ "7. Client sends PLAY (CSeq:7; single PLAY '/' for GFE≥7.1.431, else per-stream). Host replies 200 OK and the RTP video/audio + control/input flows begin on the UDP ports. First UDP datagram from client on each A/V port carries the X-SS-Ping-Payload so the host learns the client source port (NAT punch / port-learn)." ], "crypto": "RIKEY/RIKEYID origin: the Moonlight client app generates remoteInputAesKey[16] and remoteInputAesIv[16] (STREAM_CONFIGURATION in Limelight.h) before connecting; rikey = hex(remoteInputAesKey), rikeyid = a 32-bit int. They are delivered to the host NOT in RTSP but in the HTTPS /launch query string. Sunshine make_launch_session: gcm_key = from_hex_vec(rikey) (16 bytes, the AES-128-GCM key shared by video/audio/control/input/RTSP ciphers); iv (16 bytes) = big-endian uint32(rikeyid) in bytes[0..4], zero-padded. AES-128-GCM is used everywhere. Per-stream 12-byte GCM nonce construction (Sunshine stream.cpp / rtsp.cpp): VIDEO (host→client): bytes[0..]=gcm_iv_counter (LE incrementing), byte[11]='V'. CONTROL host→client: 12-byte iv = seq(LE) || byte[10]='H', byte[11]='C'. CONTROL client→host: seq(LE) || byte[10]='C', byte[11]='C'. RTSP (encrypted handshake): seq(BE in bytes[0..4]) || byte[10]='C'(client)|'H'(host), byte[11]='R'. Encryption is negotiated, not key-exchanged: DESCRIBE advertises x-ss-general.encryptionSupported (default SS_ENC_CONTROL_V2|SS_ENC_AUDIO=0x05, +SS_ENC_VIDEO=0x02 if allowed) and encryptionRequested (default SS_ENC_CONTROL_V2=0x01, +VIDEO|AUDIO if MANDATORY); the client echoes the chosen bitmask in ANNOUNCE x-ss-general.encryptionEnabled, and x-nv-general.featureFlags bit 0x20 forces SS_ENC_AUDIO on. Flags: SS_ENC_CONTROL_V2=0x01, SS_ENC_VIDEO=0x02, SS_ENC_AUDIO=0x04. Codec tag bitStreamFormat: 0=H264,1=HEVC,2=AV1 (client capability bits VIDEO_FORMAT_H264=0x0001,H265=0x0100,H265_MAIN10=0x0200,AV1_MAIN8=0x1000,AV1_MAIN10=0x2000).", - "rust_options": "For RTSP on 48010: a tiny synchronous TCP server using std::net::TcpListener on a native thread (NO tokio — keep off the hot path, consistent with lumen's no-async-on-hot-path invariant). Parse RTSP/1.0 manually: read until \\\\r\\\\n\\\\r\\\\n, split request line + headers, read Content-length body for ANNOUNCE. There is no need for a crate; a hand-rolled parser mirroring Sunshine's parseRtspMessage is simplest and avoids pulling RTSP libs that assume standard semantics (the streamid= targets and DEADBEEFCAFE session break them). For SDP build/parse, just format!/split on lines — it is line-oriented a=key:value. For the /launch HTTPS endpoint, reuse the existing crates/lumen-host/src/web.rs seam; a small hyper or tiny_http + rustls TLS server (control plane only, async OK here since it is not the hot path — matches the 'quic feature gated' precedent). For encrypted-RTSP framing use the aes-gcm crate already in lumen-core. Suggested new types: an RtspServer in lumen-host that produces a lumen_core::Config from the ANNOUNCE map. Hex decode rikey with the `hex` crate (or from_str_radix). Big-endian rikeyid → IV with u32::to_be_bytes.", - "reuse_from_lumen": "REUSE: lumen-core/src/crypto.rs SessionCrypto already implements AES-128-GCM with per-direction salting and seq-as-AAD — but NOTE it is NOT byte-compatible with GameStream's nonce layout. lumen uses nonce = salt(4) || seq(8, BE) with a direction bit folded into salt[0], whereas GameStream uses iv = seq(LE or BE per stream) with literal direction/stream marker bytes at [10]/[11] ('V', 'H'/'C'+'C', 'C'/'H'+'R'). To talk to stock Moonlight you must add a GameStream-exact nonce mode (new constructor or a feature) rather than reuse the existing salt scheme verbatim. The Aes128Gcm cipher init and seal/open plumbing are reusable. REUSE: lumen-core Config/FecConfig — fec_percent maps to GameStream's repairPercent and the recovery_for() ceil(k*pct/100) already matches GameStream's FEC math; map ANNOUNCE packetSize→shard_payload, maximumBitrateKbps→bitrate, fec.minRequiredFecPackets→minRequiredFecPackets. FecScheme::Gf8 is the GameStream-compatible field. BUILD NEW: the entire RTSP/SDP/launch negotiation layer (lumen's internal 40-byte packet format and Config are not wire-exact to RTP/RTSP); the RTSP server, SDP describe/announce codec, the /launch query parser that produces gcm_key+iv from rikey/rikeyid, and the GameStream RTP video/audio packetization + RTPFEC are all new (separate areas). The Session in lumen-core can consume the negotiated Config but its on-wire packet header must be swapped for GameStream RTP for Moonlight compat.", + "rust_options": "For RTSP on 48010: a tiny synchronous TCP server using std::net::TcpListener on a native thread (NO tokio — keep off the hot path, consistent with punktfunk's no-async-on-hot-path invariant). Parse RTSP/1.0 manually: read until \\\\r\\\\n\\\\r\\\\n, split request line + headers, read Content-length body for ANNOUNCE. There is no need for a crate; a hand-rolled parser mirroring Sunshine's parseRtspMessage is simplest and avoids pulling RTSP libs that assume standard semantics (the streamid= targets and DEADBEEFCAFE session break them). For SDP build/parse, just format!/split on lines — it is line-oriented a=key:value. For the /launch HTTPS endpoint, reuse the existing crates/punktfunk-host/src/web.rs seam; a small hyper or tiny_http + rustls TLS server (control plane only, async OK here since it is not the hot path — matches the 'quic feature gated' precedent). For encrypted-RTSP framing use the aes-gcm crate already in punktfunk-core. Suggested new types: an RtspServer in punktfunk-host that produces a punktfunk_core::Config from the ANNOUNCE map. Hex decode rikey with the `hex` crate (or from_str_radix). Big-endian rikeyid → IV with u32::to_be_bytes.", + "reuse_from_punktfunk": "REUSE: punktfunk-core/src/crypto.rs SessionCrypto already implements AES-128-GCM with per-direction salting and seq-as-AAD — but NOTE it is NOT byte-compatible with GameStream's nonce layout. punktfunk uses nonce = salt(4) || seq(8, BE) with a direction bit folded into salt[0], whereas GameStream uses iv = seq(LE or BE per stream) with literal direction/stream marker bytes at [10]/[11] ('V', 'H'/'C'+'C', 'C'/'H'+'R'). To talk to stock Moonlight you must add a GameStream-exact nonce mode (new constructor or a feature) rather than reuse the existing salt scheme verbatim. The Aes128Gcm cipher init and seal/open plumbing are reusable. REUSE: punktfunk-core Config/FecConfig — fec_percent maps to GameStream's repairPercent and the recovery_for() ceil(k*pct/100) already matches GameStream's FEC math; map ANNOUNCE packetSize→shard_payload, maximumBitrateKbps→bitrate, fec.minRequiredFecPackets→minRequiredFecPackets. FecScheme::Gf8 is the GameStream-compatible field. BUILD NEW: the entire RTSP/SDP/launch negotiation layer (punktfunk's internal 40-byte packet format and Config are not wire-exact to RTP/RTSP); the RTSP server, SDP describe/announce codec, the /launch query parser that produces gcm_key+iv from rikey/rikeyid, and the GameStream RTP video/audio packetization + RTPFEC are all new (separate areas). The Session in punktfunk-core can consume the negotiated Config but its on-wire packet header must be swapped for GameStream RTP for Moonlight compat.", "gotchas": [ - "lumen-core's AES-GCM nonce layout is NOT GameStream-compatible (salt+BE-seq vs literal 'V'/'C'/'R' marker bytes + LE/BE seq). A stock Moonlight will fail auth unless you implement the exact per-stream IV construction. This is the single biggest bridging hazard.", + "punktfunk-core's AES-GCM nonce layout is NOT GameStream-compatible (salt+BE-seq vs literal 'V'/'C'/'R' marker bytes + LE/BE seq). A stock Moonlight will fail auth unless you implement the exact per-stream IV construction. This is the single biggest bridging hazard.", "rikeyid is parsed as a SIGNED int then cast to big-endian uint32 in Sunshine (util::from_view → int → endian::big). Negative rikeyid values wrap; match the signed-int→BE-u32 path exactly.", "The IV from /launch (BE32(rikeyid)||zeros, 16 bytes) is the *base*; the actual per-packet 12-byte GCM nonce is rebuilt per stream with seq + marker bytes — do not just use the 16-byte launch IV directly.", "Session header value is the literal 'DEADBEEFCAFE;timeout = 90' WITH spaces around '='. Moonlight is lenient but match it.", @@ -179,7 +179,7 @@ "moonlight-common-c src/Limelight-internal.h — SS_ENC_CONTROL_V2 0x01, SS_ENC_VIDEO 0x02, SS_ENC_AUDIO 0x04; RtspPortNumber/Control/Audio/VideoPortNumber externs", "moonlight-common-c src/Connection.c — port number init (set from RTSP SETUP), resolveHostName base 47984", "DeepWiki LizardByte/Sunshine port management — base 47989 with offsets: video 47998 (+9), control 47999 (+10), audio 48000 (+11), RTSP 48010 (+21) via net::map_port (1024-65535 validated)", - "lumen-core src/crypto.rs and src/config.rs (read directly) — existing AES-128-GCM SessionCrypto (salt+BE-seq nonce, NOT GameStream-exact) and Config/FecConfig for reuse assessment" + "punktfunk-core src/crypto.rs and src/config.rs (read directly) — existing AES-128-GCM SessionCrypto (salt+BE-seq nonce, NOT GameStream-exact) and Config/FecConfig for reuse assessment" ] }, { @@ -245,15 +245,15 @@ "When a block has >= dataShards shards (data+parity), client runs nanors reed_solomon_decode(rs, packets, marks, totalPackets, blocksize) to recover any missing data shards (missing slots zero-filled, marks[]=1 for missing). Recovered data shards get synthetic RTP headers (seq/header/timestamp/ssrc copied from a present packet).", "Client advances through blocks (currentBlock 0..lastBlock); once all blocks' data shards are present/recovered it strips the 32-byte video_packet_raw_t header off each data shard, concatenates payloads in sequence order, parses the 8-byte short frame header, and hands the reassembled access unit to the depacketizer/decoder." ], - "crypto": "\"Cipher: AES-128-GCM (EVP_aes_128_gcm). Key: the 16-byte RIKEY — Sunshine `launch_session.gcm_key`, Moonlight `StreamConfig.remoteInputAesKey[16]` — established during RTSP/pairing; the SAME key is used for the control stream and (if enabled) audio. There is no separate video key and no key derivation: the raw 16-byte RIKEY is used directly. IV/nonce: 12 bytes, constructed deterministically (NIST SP 800-38D 8.2.1): iv[0..8] = a 64-bit per-session counter (session->video.gcm_iv_counter, starts at 0) copied in NATIVE byte order (little-endian on x86), iv[8..11] = 0 except iv[11] = 'V' (0x56, the video-stream fixed field). The counter increments once per shard. The full 12-byte IV is transmitted in the ENC_VIDEO_HEADER so the client uses it verbatim (it does not reconstruct it). Tag: 16 bytes, standard GCM tag, transmitted in ENC_VIDEO_HEADER. AAD / associated data: NONE — Sunshine's gcm_t::encrypt for video calls EVP_EncryptUpdate only with plaintext (no AAD update), and Moonlight's PltDecryptMessage passes no AAD argument. (NOTE: this differs from lumen-core/crypto.rs which uses seq-as-AAD and a per-direction salt; GameStream video does neither.) Order: FEC FIRST, THEN ENCRYPT — encryption is applied per-shard after RS parity is computed, over the entire blocksize shard buffer (RTP+NV+payload), so the client must DECRYPT each shard before it can run FEC reconstruction. Encrypted plaintext length == blocksize; ciphertext length == blocksize (GCM is a stream cipher, no expansion); on the wire the packet grows only by the 32-byte prefix.\"", - "rust_options": "\"FEC math: lumen already has Gf8Coder over `reed-solomon-erasure` (galois_8). CRITICAL RISK: this is NOT guaranteed byte-compatible with nanors. Both Sunshine and Moonlight use nanors (Sunshine's rswrapper.h is 'a drop-in replacement for nanors rs.h', DATA_SHARDS_MAX=255), which uses a specific GF(2^8) field (primitive poly 0x11d) and a Vandermonde-derived generator matrix with a particular systematic encoding. `reed-solomon-erasure` uses Cauchy matrices by default and may produce DIFFERENT parity bytes — meaning Moonlight would FAIL to recover frames where any data shard is lost. RECOMMENDED: vendor/FFI the actual nanors C library (it is tiny, MIT, header+rs.c+oblas) and call reed_solomon_new/encode through a thin Rust FFI, OR port nanors' matrix construction exactly into a new gf8 backend. Do NOT assume reed-solomon-erasure interop without a byte-for-byte test against nanors output. (For lumen-to-lumen P2 traffic, keep the existing coders; for GameStream-client compat, use nanors.) Crypto: use `aes-gcm` (already a dep) but build a NEW path that (a) takes the raw RIKEY as the key, (b) builds the 12-byte IV as counter_le[8]||0||0||0||'V', (c) uses NO AAD, (d) does NOT use the per-direction salt logic in SessionCrypto. The cleanest approach is a small standalone `Aes128Gcm` call rather than reusing SessionCrypto (whose nonce/AAD scheme is incompatible). Byte layout: define `#[repr(C, packed)]` structs RtpPacket, NvVideoPacket, EncVideoHeader and use explicit `to_be_bytes`/`to_le_bytes` per field (RTP=BE, NV=LE) — do not rely on struct memory layout for endianness.\"", - "reuse_from_lumen": "\"REUSE the GF(2^8) concept/structure but NOT necessarily the implementation: lumen's `ErasureCoder` trait and `Gf8Coder` (reed-solomon-erasure) give the right data||parity systematic layout and the 255-shard ceiling, but parity bytes likely won't match nanors — so for client-facing GameStream compat add a `nanors`-backed coder (FFI or exact port) behind the same trait. The trait's reconstruct(data_count, recovery_count, received: indices 0..K originals, K..K+M recovery) maps cleanly onto Moonlight's layout (data shards at RS index 0..dataShards, parity after) so the adapter just needs to map RTP-seq→shard-index. REUSE aes-gcm crate but NOT SessionCrypto (its salt+seq-AAD scheme is wire-incompatible with GameStream video which uses no AAD and a counter||'V' IV). REUSE lumen's UDP transport for sending datagrams. Do NOT reuse lumen's internal 40-byte packet format — GameStream needs the exact 12+4+16 header + optional 32-byte enc prefix. NEW work: a `gamestream` wire module in lumen-host (NOT lumen-core, to keep lumen-core's clean internal protocol) that (1) builds RTP_PACKET/NV_VIDEO_PACKET/ENC_VIDEO_HEADER bytes, (2) implements the frame→FEC-block split (max 4 blocks, the 255/(1+F) shard math), (3) drives a nanors coder, (4) does the per-shard counter-IV AES-128-GCM-no-AAD encrypt, (5) paces/batches sends. Best location: a new file like crates/lumen-host/src/gamestream/video.rs (host side) with the nanors FFI either in lumen-host or a small new crate; lumen-core stays the lumen-native protocol and only its aes-gcm + the gf8 *math* are conceptually shared. The 'adapter' lives at the lumen-host pipeline seam: take the NVENC access unit + frame metadata from encode.rs and emit GameStream datagrams instead of (or alongside) lumen-native packets.\"", + "crypto": "\"Cipher: AES-128-GCM (EVP_aes_128_gcm). Key: the 16-byte RIKEY — Sunshine `launch_session.gcm_key`, Moonlight `StreamConfig.remoteInputAesKey[16]` — established during RTSP/pairing; the SAME key is used for the control stream and (if enabled) audio. There is no separate video key and no key derivation: the raw 16-byte RIKEY is used directly. IV/nonce: 12 bytes, constructed deterministically (NIST SP 800-38D 8.2.1): iv[0..8] = a 64-bit per-session counter (session->video.gcm_iv_counter, starts at 0) copied in NATIVE byte order (little-endian on x86), iv[8..11] = 0 except iv[11] = 'V' (0x56, the video-stream fixed field). The counter increments once per shard. The full 12-byte IV is transmitted in the ENC_VIDEO_HEADER so the client uses it verbatim (it does not reconstruct it). Tag: 16 bytes, standard GCM tag, transmitted in ENC_VIDEO_HEADER. AAD / associated data: NONE — Sunshine's gcm_t::encrypt for video calls EVP_EncryptUpdate only with plaintext (no AAD update), and Moonlight's PltDecryptMessage passes no AAD argument. (NOTE: this differs from punktfunk-core/crypto.rs which uses seq-as-AAD and a per-direction salt; GameStream video does neither.) Order: FEC FIRST, THEN ENCRYPT — encryption is applied per-shard after RS parity is computed, over the entire blocksize shard buffer (RTP+NV+payload), so the client must DECRYPT each shard before it can run FEC reconstruction. Encrypted plaintext length == blocksize; ciphertext length == blocksize (GCM is a stream cipher, no expansion); on the wire the packet grows only by the 32-byte prefix.\"", + "rust_options": "\"FEC math: punktfunk already has Gf8Coder over `reed-solomon-erasure` (galois_8). CRITICAL RISK: this is NOT guaranteed byte-compatible with nanors. Both Sunshine and Moonlight use nanors (Sunshine's rswrapper.h is 'a drop-in replacement for nanors rs.h', DATA_SHARDS_MAX=255), which uses a specific GF(2^8) field (primitive poly 0x11d) and a Vandermonde-derived generator matrix with a particular systematic encoding. `reed-solomon-erasure` uses Cauchy matrices by default and may produce DIFFERENT parity bytes — meaning Moonlight would FAIL to recover frames where any data shard is lost. RECOMMENDED: vendor/FFI the actual nanors C library (it is tiny, MIT, header+rs.c+oblas) and call reed_solomon_new/encode through a thin Rust FFI, OR port nanors' matrix construction exactly into a new gf8 backend. Do NOT assume reed-solomon-erasure interop without a byte-for-byte test against nanors output. (For punktfunk-to-punktfunk P2 traffic, keep the existing coders; for GameStream-client compat, use nanors.) Crypto: use `aes-gcm` (already a dep) but build a NEW path that (a) takes the raw RIKEY as the key, (b) builds the 12-byte IV as counter_le[8]||0||0||0||'V', (c) uses NO AAD, (d) does NOT use the per-direction salt logic in SessionCrypto. The cleanest approach is a small standalone `Aes128Gcm` call rather than reusing SessionCrypto (whose nonce/AAD scheme is incompatible). Byte layout: define `#[repr(C, packed)]` structs RtpPacket, NvVideoPacket, EncVideoHeader and use explicit `to_be_bytes`/`to_le_bytes` per field (RTP=BE, NV=LE) — do not rely on struct memory layout for endianness.\"", + "reuse_from_punktfunk": "\"REUSE the GF(2^8) concept/structure but NOT necessarily the implementation: punktfunk's `ErasureCoder` trait and `Gf8Coder` (reed-solomon-erasure) give the right data||parity systematic layout and the 255-shard ceiling, but parity bytes likely won't match nanors — so for client-facing GameStream compat add a `nanors`-backed coder (FFI or exact port) behind the same trait. The trait's reconstruct(data_count, recovery_count, received: indices 0..K originals, K..K+M recovery) maps cleanly onto Moonlight's layout (data shards at RS index 0..dataShards, parity after) so the adapter just needs to map RTP-seq→shard-index. REUSE aes-gcm crate but NOT SessionCrypto (its salt+seq-AAD scheme is wire-incompatible with GameStream video which uses no AAD and a counter||'V' IV). REUSE punktfunk's UDP transport for sending datagrams. Do NOT reuse punktfunk's internal 40-byte packet format — GameStream needs the exact 12+4+16 header + optional 32-byte enc prefix. NEW work: a `gamestream` wire module in punktfunk-host (NOT punktfunk-core, to keep punktfunk-core's clean internal protocol) that (1) builds RTP_PACKET/NV_VIDEO_PACKET/ENC_VIDEO_HEADER bytes, (2) implements the frame→FEC-block split (max 4 blocks, the 255/(1+F) shard math), (3) drives a nanors coder, (4) does the per-shard counter-IV AES-128-GCM-no-AAD encrypt, (5) paces/batches sends. Best location: a new file like crates/punktfunk-host/src/gamestream/video.rs (host side) with the nanors FFI either in punktfunk-host or a small new crate; punktfunk-core stays the punktfunk-native protocol and only its aes-gcm + the gf8 *math* are conceptually shared. The 'adapter' lives at the punktfunk-host pipeline seam: take the NVENC access unit + frame metadata from encode.rs and emit GameStream datagrams instead of (or alongside) punktfunk-native packets.\"", "gotchas": [ "ENDIANNESS SPLIT: RTP_PACKET fields are BIG-endian, NV_VIDEO_PACKET fields are LITTLE-endian, within the SAME packet. Easy to get wrong. RTP: BE16/BE32; NV streamPacketIndex/frameIndex/fecInfo: LE32.", "ENCRYPT-AFTER-FEC, not before: the GCM-encrypted region is the WHOLE shard (RTP+NV+payload) and the client must decrypt each shard before FEC. The 32-byte ENC_VIDEO_HEADER is a wire PREFIX outside the FEC blocksize, not part of the protected data. If you FEC after encrypt or include the prefix in the FEC math, recovery breaks.", - "NO AAD on video GCM — unlike lumen-core's SessionCrypto which authenticates the sequence number as AAD. Using SessionCrypto verbatim will fail Moonlight's tag check.", + "NO AAD on video GCM — unlike punktfunk-core's SessionCrypto which authenticates the sequence number as AAD. Using SessionCrypto verbatim will fail Moonlight's tag check.", "IV counter byte order: Sunshine copies the 64-bit counter with std::copy_n in NATIVE order (little-endian on the x86 build), so iv[0..8] is the counter LE; iv[11]='V'(0x56), iv[8..11]=0. The client uses the transmitted iv verbatim, so as long as you SEND the iv you used, internal byte order is self-consistent — but match LE to mirror Sunshine exactly and to keep nonces unique.", - "FEC parity matrix must match nanors EXACTLY. reed-solomon-erasure (lumen's current backend) is likely NOT byte-compatible (Cauchy vs nanors Vandermonde). Without a byte-for-byte match, Moonlight silently fails to recover any frame with a lost data shard. Validate against real nanors output or FFI nanors.", + "FEC parity matrix must match nanors EXACTLY. reed-solomon-erasure (punktfunk's current backend) is likely NOT byte-compatible (Cauchy vs nanors Vandermonde). Without a byte-for-byte match, Moonlight silently fails to recover any frame with a lost data shard. Validate against real nanors output or FFI nanors.", "streamPacketIndex is (lowseq+x)<<8 with low byte zero; client does >>=8 then &0xFFFFFF → a 24-bit stream-wide packet index, distinct from the 16-bit RTP sequenceNumber. Both must be set consistently or the depacketizer's continuity check (FLAG_SOF / streamPacketIndex == lastPacketInStream+1) rejects the frame.", "Max 4 FEC blocks per frame (2-bit fields). Max 1024 packets per block (10-bit fecIndex). Max 255 shards/block (GF(2^8)). data_shards per block = 255*100/(100+fecPercentage). Exceeding these → FEC disabled or unrecoverable frame.", "Last data shard is zero-padded to blocksize before RS encode; lastPayloadLen in the short frame header tells the client the real length of the final packet's payload (needed for AV1). Padding must be zeros so RS math and the client's memset-padding agree.", @@ -274,7 +274,7 @@ "moonlight-common-c src/Limelight.h — ENCFLG_VIDEO=0x2, ENCFLG_AUDIO=0x1, remoteInputAesKey[16] (the RIKEY).", "moonlight-common-c src/SdpGenerator.c — SS_ENC_VIDEO negotiation; when enabled StreamConfig.packetSize -= sizeof(ENC_VIDEO_HEADER) (32).", "moonlight-common-c .gitmodules + repo tree — nanors at /nanors (rs.c, rs.h, deps/obl GF(2^8) tables), confirming both host(Sunshine via rswrapper) and client use nanors.", - "lumen local: crates/lumen-core/src/crypto.rs (SessionCrypto: salt+seq-AAD scheme — incompatible with GameStream video), crates/lumen-core/src/fec/{mod.rs,gf8.rs} (ErasureCoder trait + reed-solomon-erasure galois_8 — needs nanors compat verification)." + "punktfunk local: crates/punktfunk-core/src/crypto.rs (SessionCrypto: salt+seq-AAD scheme — incompatible with GameStream video), crates/punktfunk-core/src/fec/{mod.rs,gf8.rs} (ErasureCoder trait + reed-solomon-erasure galois_8 — needs nanors compat verification)." ] }, { @@ -320,10 +320,10 @@ "Client decrypts recovered/received data shards (AES-128-CBC, strip PKCS7), reorders by sequence, and feeds frames to opus_multistream_decoder_create/decode_float." ], "crypto": "Cipher: AES-128-CBC (NOT GCM — video & control use GCM, audio uses CBC). Key: 16 bytes = the session AES key (Sunshine launch_session.gcm_key / moonlight StreamConfig.remoteInputAesKey), same key family used for the input/RI channel. IV: 16 bytes, per-packet — first 4 bytes = big-endian uint32 of (avRiKeyId + sequenceNumber), remaining 12 bytes = 0x00. avRiKeyId is a per-session 32-bit value from the RTSP/launch negotiation (session->audio.avRiKeyId). Padding: PKCS7 to AES block (16-byte) multiple; round_to_pkcs7_padded, max_block_size = round_to_pkcs7_padded(2048). No GMAC/auth tag is appended to audio packets. Encryption is gated by the SS_ENC_AUDIO encryption flag (config.encryptionFlagsEnabled & SS_ENC_AUDIO) — if disabled, raw Opus frame is sent. FEC parity is computed over the post-encryption, post-padding shard bytes so recovery yields ciphertext that is then decrypted.", - "rust_options": "Opus encode: use the `audiopus` crate or `opus` (libopus bindings) — both expose multistream via opus_multistream_encoder; if missing, FFI to libopus opus_multistream_encoder_create/opus_multistream_encode_float directly. Configure sampleRate=48000, the streams/coupledStreams and mapping per the negotiated AUDIO_CONFIGURATION; frame size = 48*packetDuration samples/ch. AES-128-CBC: use the `aes` + `cbc` crates (cbc::Encryptor) with manual PKCS7 (`block-padding`/`Pkcs7`) — build the 16-byte IV as BE32(avRiKeyId+seq) || [0u8;12]. Reed-Solomon: do NOT use a generic RS matrix; the wire requires Nvidia's specific parity matrix. The `reed-solomon-erasure` crate computes its own (Vandermonde/Cauchy) matrix that will NOT match — either (a) port moonlight's approach: take the rs lib's encode path but inject the OpenFEC parity matrix bytes {0x77,0x40,0x38,0x0e,0xc7,0xa7,0x0d,0x6c} for the 4+2 case, or (b) hand-roll a tiny GF(2^8) 4-data/2-parity encoder/decoder using that exact 2x4 parity matrix (8 bytes = 2 parity rows × 4 data cols). lumen's existing `reed-solomon` GF(2^8) code can be reused ONLY if it lets you supply a custom generator/parity matrix; otherwise add a dedicated audio-FEC path. Big-endian field writes: use `byteorder`/`to_be_bytes`. UDP: std::net::UdpSocket on a native thread (no async, matching lumen's hot-path rule).", - "reuse_from_lumen": "REUSE: lumen-core's AES-128 primitive (crypto.rs) underlies CBC, but the MODE differs — lumen uses AES-128-GCM with per-direction nonce salts + seq-as-AAD; GameStream audio needs AES-128-CBC with the BE32(avRiKeyId+seq) IV and PKCS7, no AAD/tag. So add a CBC path; do not reuse the GCM nonce/AAD scheme for audio. lumen's GF(2^8) Reed-Solomon (reed-solomon crate) is the right field but the MATRIX is wrong for the wire — must supply Nvidia's hardcoded {0x77,0x40,0x38,0x0e,0xc7,0xa7,0x0d,0x6c} parity matrix or hand-roll the 4+2 encoder; lumen's internal 40-byte packet format and its FEC block sizing are NOT wire-compatible and cannot be reused for the on-wire audio packets. lumen's UDP transport + native-thread pacing model is reusable as plumbing. NEW: 12-byte RTP header serializer, 12-byte AUDIO_FEC_HEADER, the fixed 4+2 audio FEC block state machine (block starts at seq%4==0, encode at (seq+1)%4==0), the Opus multistream encoder integration, the 500 ms ping listener, and the AES-CBC+PKCS7 audio path. These are GameStream-specific and don't exist in lumen-core.", + "rust_options": "Opus encode: use the `audiopus` crate or `opus` (libopus bindings) — both expose multistream via opus_multistream_encoder; if missing, FFI to libopus opus_multistream_encoder_create/opus_multistream_encode_float directly. Configure sampleRate=48000, the streams/coupledStreams and mapping per the negotiated AUDIO_CONFIGURATION; frame size = 48*packetDuration samples/ch. AES-128-CBC: use the `aes` + `cbc` crates (cbc::Encryptor) with manual PKCS7 (`block-padding`/`Pkcs7`) — build the 16-byte IV as BE32(avRiKeyId+seq) || [0u8;12]. Reed-Solomon: do NOT use a generic RS matrix; the wire requires Nvidia's specific parity matrix. The `reed-solomon-erasure` crate computes its own (Vandermonde/Cauchy) matrix that will NOT match — either (a) port moonlight's approach: take the rs lib's encode path but inject the OpenFEC parity matrix bytes {0x77,0x40,0x38,0x0e,0xc7,0xa7,0x0d,0x6c} for the 4+2 case, or (b) hand-roll a tiny GF(2^8) 4-data/2-parity encoder/decoder using that exact 2x4 parity matrix (8 bytes = 2 parity rows × 4 data cols). punktfunk's existing `reed-solomon` GF(2^8) code can be reused ONLY if it lets you supply a custom generator/parity matrix; otherwise add a dedicated audio-FEC path. Big-endian field writes: use `byteorder`/`to_be_bytes`. UDP: std::net::UdpSocket on a native thread (no async, matching punktfunk's hot-path rule).", + "reuse_from_punktfunk": "REUSE: punktfunk-core's AES-128 primitive (crypto.rs) underlies CBC, but the MODE differs — punktfunk uses AES-128-GCM with per-direction nonce salts + seq-as-AAD; GameStream audio needs AES-128-CBC with the BE32(avRiKeyId+seq) IV and PKCS7, no AAD/tag. So add a CBC path; do not reuse the GCM nonce/AAD scheme for audio. punktfunk's GF(2^8) Reed-Solomon (reed-solomon crate) is the right field but the MATRIX is wrong for the wire — must supply Nvidia's hardcoded {0x77,0x40,0x38,0x0e,0xc7,0xa7,0x0d,0x6c} parity matrix or hand-roll the 4+2 encoder; punktfunk's internal 40-byte packet format and its FEC block sizing are NOT wire-compatible and cannot be reused for the on-wire audio packets. punktfunk's UDP transport + native-thread pacing model is reusable as plumbing. NEW: 12-byte RTP header serializer, 12-byte AUDIO_FEC_HEADER, the fixed 4+2 audio FEC block state machine (block starts at seq%4==0, encode at (seq+1)%4==0), the Opus multistream encoder integration, the 500 ms ping listener, and the AES-CBC+PKCS7 audio path. These are GameStream-specific and don't exist in punktfunk-core.", "gotchas": [ - "AES-CBC, not GCM. Audio is the one stream using CBC; reusing lumen's GCM code verbatim will break interop. No auth tag is on the wire for audio.", + "AES-CBC, not GCM. Audio is the one stream using CBC; reusing punktfunk's GCM code verbatim will break interop. No auth tag is on the wire for audio.", "IV is only 4 meaningful bytes: BE32(avRiKeyId + sequenceNumber) then 12 zero bytes. The addition wraps as uint32. avRiKeyId is per-session from RTSP launch.", "The Reed-Solomon parity matrix MUST be Nvidia's hardcoded one. moonlight explicitly notes 'the RS parity matrix computed by our RS implementation doesn't match the one Nvidia uses' and overrides it with the 8 OpenFEC bytes {0x77,0x40,0x38,0x0e,0xc7,0xa7,0x0d,0x6c}. A stock reed-solomon-erasure encoder will produce parity the client cannot decode.", "FEC is a FIXED 4+2 block (RTPA_DATA_SHARDS=4, RTPA_FEC_SHARDS=2), unlike video which uses dynamic/large blocks and multi-FEC. Block boundaries must align to seq%4==0 or the client's queue logic rejects them.", @@ -352,8 +352,8 @@ ] }, { - "area": "GameStream wire-format gap analysis + architecture recommendation for lumen-host (P1 / M2)", - "summary": "lumen-core today speaks an INTERNAL protocol that is structurally similar to GameStream but byte-incompatible on every wire surface, so a stock Moonlight client cannot connect to it as-is. Differences: (1) lumen prefixes each shard with a 40-byte little-endian `PacketHeader` and no RTP layer; GameStream uses a 12-byte big-endian RTP header + 4 reserved bytes + a 16-byte `NV_VIDEO_PACKET` (28 bytes total) carrying frameIndex/streamPacketIndex/flags and the FEC params bit-packed into a single `fecInfo` u32 and `multiFecBlocks` u8. (2) lumen's RS-FEC interleaves data+recovery shards within one block keyed by `shard_index`; GameStream packs ALL data shards first then ALL parity shards across a contiguous RTP sequence range, derives (data,parity,fecIndex,pct) from `fecInfo`, splits a frame into up to 4 FEC blocks via `multiFecBlocks`, and the data shards must be the literal RTP-framed bytes of the H.264/HEVC NAL slices (the depacketizer concatenates payloads to rebuild Annex-B). (3) lumen seals the whole 40-byte+payload packet under AES-128-GCM with an 8-byte seq prefix and seq-as-AAD; GameStream encrypts only the post-RTP payload, prefixing a `video_packet_enc_prefix_t {iv[12]; u32 frameNumber; u8 tag[16]}` where the IV is an 8-byte little-endian per-stream counter with iv[11]='V'. The RS math itself is identical (ceil(k*pct/100), GF(2^8), <=255 shards) so lumen's `reed-solomon` GF(2^8) coder CAN produce Moonlight-recoverable parity, but ONLY if lumen abandons its own shard layout and emits shards in GameStream's data-then-parity contiguous order with GameStream's exact shard size (packetSize + 4 reserved + RTP). Beyond video, GameStream needs an entire control plane lumen has not started: HTTPS:47984/HTTP:47989 nvhttp pairing (PIN->AES-128 via SHA-256(salt||pin)[..16], ECB challenge exchange, RSA-signed client cert), an RTSP:48010 handshake (OPTIONS/DESCRIBE/SETUP/ANNOUNCE/PLAY) carrying SDP `x-nv-*` params, an ENet control stream (UDP 48000) with its own AES-128-GCM framing and opcodes (request-IDR, loss-stats, ping, HDR, termination, rumble), an AES-CBC audio stream (UDP 47999), and mDNS `_nvstream._tcp` advertisement. Recommendation: put the GameStream video/FEC/crypto wire codec as a P1 \"wire mode\" INSIDE lumen-core (the invariant says protocol logic lives in the core), but keep the stateful control plane (nvhttp/RTSP/ENet/pairing/mDNS) in lumen-host as a tokio control-plane adapter that calls into core codec functions, because that machinery is I/O-bound, async, and not part of the hot path.", + "area": "GameStream wire-format gap analysis + architecture recommendation for punktfunk-host (P1 / M2)", + "summary": "punktfunk-core today speaks an INTERNAL protocol that is structurally similar to GameStream but byte-incompatible on every wire surface, so a stock Moonlight client cannot connect to it as-is. Differences: (1) punktfunk prefixes each shard with a 40-byte little-endian `PacketHeader` and no RTP layer; GameStream uses a 12-byte big-endian RTP header + 4 reserved bytes + a 16-byte `NV_VIDEO_PACKET` (28 bytes total) carrying frameIndex/streamPacketIndex/flags and the FEC params bit-packed into a single `fecInfo` u32 and `multiFecBlocks` u8. (2) punktfunk's RS-FEC interleaves data+recovery shards within one block keyed by `shard_index`; GameStream packs ALL data shards first then ALL parity shards across a contiguous RTP sequence range, derives (data,parity,fecIndex,pct) from `fecInfo`, splits a frame into up to 4 FEC blocks via `multiFecBlocks`, and the data shards must be the literal RTP-framed bytes of the H.264/HEVC NAL slices (the depacketizer concatenates payloads to rebuild Annex-B). (3) punktfunk seals the whole 40-byte+payload packet under AES-128-GCM with an 8-byte seq prefix and seq-as-AAD; GameStream encrypts only the post-RTP payload, prefixing a `video_packet_enc_prefix_t {iv[12]; u32 frameNumber; u8 tag[16]}` where the IV is an 8-byte little-endian per-stream counter with iv[11]='V'. The RS math itself is identical (ceil(k*pct/100), GF(2^8), <=255 shards) so punktfunk's `reed-solomon` GF(2^8) coder CAN produce Moonlight-recoverable parity, but ONLY if punktfunk abandons its own shard layout and emits shards in GameStream's data-then-parity contiguous order with GameStream's exact shard size (packetSize + 4 reserved + RTP). Beyond video, GameStream needs an entire control plane punktfunk has not started: HTTPS:47984/HTTP:47989 nvhttp pairing (PIN->AES-128 via SHA-256(salt||pin)[..16], ECB challenge exchange, RSA-signed client cert), an RTSP:48010 handshake (OPTIONS/DESCRIBE/SETUP/ANNOUNCE/PLAY) carrying SDP `x-nv-*` params, an ENet control stream (UDP 48000) with its own AES-128-GCM framing and opcodes (request-IDR, loss-stats, ping, HDR, termination, rumble), an AES-CBC audio stream (UDP 47999), and mDNS `_nvstream._tcp` advertisement. Recommendation: put the GameStream video/FEC/crypto wire codec as a P1 \"wire mode\" INSIDE punktfunk-core (the invariant says protocol logic lives in the core), but keep the stateful control plane (nvhttp/RTSP/ENet/pairing/mDNS) in punktfunk-host as a tokio control-plane adapter that calls into core codec functions, because that machinery is I/O-bound, async, and not part of the hot path.", "ports": [ "TCP 47984 — HTTPS nvhttp (paired control: /serverinfo, /pair, /applist, /launch, /resume, /cancel). Client-cert pinned to the paired client.", "TCP 47989 — HTTP nvhttp (unpaired: /serverinfo unauthenticated, /pair PIN flow).", @@ -362,38 +362,38 @@ "UDP 47999 — Audio RTP stream (Opus, AES-CBC, RS-FEC). ML_PORT_INDEX_UDP_47999=9.", "UDP 48000 — ENet control stream (reliable, AES-128-GCM, opcodes). ML_PORT_INDEX_UDP_48000=10.", "UDP/mDNS 5353 — _nvstream._tcp.local advertisement so Moonlight auto-discovers the host.", - "Note: Moonlight derives all of these by offset from the HTTP base port (default 47989); changing the base shifts the whole set. lumen-host must advertise the actual HttpsPort/ExternalPort in serverinfo XML." + "Note: Moonlight derives all of these by offset from the HTTP base port (default 47989); changing the base shifts the whole set. punktfunk-host must advertise the actual HttpsPort/ExternalPort in serverinfo XML." ], "wire_formats": [ { - "name": "DELTA: video packet header (lumen vs GameStream)", - "layout": "lumen PacketHeader = 40 bytes, little-endian, repr(C): pts_ns u64, frame_index u32, stream_seq u32, frame_bytes u32, user_flags u32, block_index u16, block_count u16, data_shards u16, recovery_shards u16, shard_index u16, shard_bytes u16, magic u8(0xC9), version u8, fec_scheme u8, flags u8. || GameStream on-wire = RTP_PACKET(12, big-endian: u8 header, u8 packetType, u16 sequenceNumber, u32 timestamp, u32 ssrc) + char reserved[4] + NV_VIDEO_PACKET(16, little-endian: u32 streamPacketIndex@0, u32 frameIndex@4, u8 flags@8, u8 extraFlags@9 (NV_VIDEO_PACKET_EXTRA_FLAG_LTR_FRAME=0x1), u8 multiFecFlags@10, u8 multiFecBlocks@11, u32 fecInfo@12) = 28 bytes before payload.", - "notes": "DELTA: drop pts_ns/frame_bytes/shard_bytes from the wire (GameStream carries none of these per-packet); add the RTP header + reserved[4]; replace explicit u16 FEC fields with the bit-packed fecInfo+multiFecBlocks. flags map 1:1 (FLAG_CONTAINS_PIC_DATA=0x1==lumen FLAG_PIC, FLAG_EOF=0x2, FLAG_SOF=0x4). frameIndex == lumen frame_index; streamPacketIndex == per-stream packet counter (NOT lumen stream_seq which is per-AU)." + "name": "DELTA: video packet header (punktfunk vs GameStream)", + "layout": "punktfunk PacketHeader = 40 bytes, little-endian, repr(C): pts_ns u64, frame_index u32, stream_seq u32, frame_bytes u32, user_flags u32, block_index u16, block_count u16, data_shards u16, recovery_shards u16, shard_index u16, shard_bytes u16, magic u8(0xC9), version u8, fec_scheme u8, flags u8. || GameStream on-wire = RTP_PACKET(12, big-endian: u8 header, u8 packetType, u16 sequenceNumber, u32 timestamp, u32 ssrc) + char reserved[4] + NV_VIDEO_PACKET(16, little-endian: u32 streamPacketIndex@0, u32 frameIndex@4, u8 flags@8, u8 extraFlags@9 (NV_VIDEO_PACKET_EXTRA_FLAG_LTR_FRAME=0x1), u8 multiFecFlags@10, u8 multiFecBlocks@11, u32 fecInfo@12) = 28 bytes before payload.", + "notes": "DELTA: drop pts_ns/frame_bytes/shard_bytes from the wire (GameStream carries none of these per-packet); add the RTP header + reserved[4]; replace explicit u16 FEC fields with the bit-packed fecInfo+multiFecBlocks. flags map 1:1 (FLAG_CONTAINS_PIC_DATA=0x1==punktfunk FLAG_PIC, FLAG_EOF=0x2, FLAG_SOF=0x4). frameIndex == punktfunk frame_index; streamPacketIndex == per-stream packet counter (NOT punktfunk stream_seq which is per-AU)." }, { "name": "fecInfo bit-packing (GameStream, exact)", "layout": "fecInfo (u32, little-endian field) = (dataShards << 22) | (fecIndex << 12) | (fecPercentage << 4). Decode masks (moonlight-common-c): dataShards=(fecInfo & 0xFFC00000)>>22 (bits 22-31, 10 bits, <=1023 but RS caps at 255); fecIndex=(fecInfo & 0x3FF000)>>12 (bits 12-21, the shard's index within its block); fecPercentage=(fecInfo & 0xFF0)>>4 (bits 4-11). parityShards = (dataShards*fecPercentage + 99)/100 (ceiling). bits 0-3 unused.", - "notes": "This is IDENTICAL math to lumen's FecConfig::recovery_for (ceil(k*pct/100)). lumen already computes data_shards/recovery_shards as explicit u16; the only delta is packing them into this bitfield and emitting fecIndex as the contiguous index across [0..data) then [data..data+parity)." + "notes": "This is IDENTICAL math to punktfunk's FecConfig::recovery_for (ceil(k*pct/100)). punktfunk already computes data_shards/recovery_shards as explicit u16; the only delta is packing them into this bitfield and emitting fecIndex as the contiguous index across [0..data) then [data..data+parity)." }, { "name": "multiFecBlocks bit-packing (GameStream, exact)", "layout": "multiFecBlocks (u8) = (blockIndex << 4) | ((fec_blocks_needed - 1) << 6). Decode: fecCurrentBlockNumber=(multiFecBlocks>>4)&0x3; lastBlockNumber=(multiFecBlocks>>6)&0x3. Max 4 FEC blocks per frame (2 bits each).", - "notes": "DELTA vs lumen: lumen uses u16 block_index/block_count with no 4-block ceiling. For P1 wire mode, max_data_per_block must be chosen so a frame needs <=4 blocks AND each block <=255 total shards. lumen's p1_defaults (max_data_per_block=200, 15% FEC -> 230 total) already respects 255; just cap blocks at 4 for GameStream mode." + "notes": "DELTA vs punktfunk: punktfunk uses u16 block_index/block_count with no 4-block ceiling. For P1 wire mode, max_data_per_block must be chosen so a frame needs <=4 blocks AND each block <=255 total shards. punktfunk's p1_defaults (max_data_per_block=200, 15% FEC -> 230 total) already respects 255; just cap blocks at 4 for GameStream mode." }, { "name": "RS-FEC shard arrangement (the recoverability question)", "layout": "GameStream: within one FEC block, RTP sequence numbers are contiguous: data shards occupy [bufferLowestSequenceNumber .. bufferFirstParitySequenceNumber-1], parity shards immediately follow. totalPackets = highest-lowest+1 = dataShards+parityShards. Each shard is exactly receiveSize = packetSize + MAX_RTP_HEADER_SIZE bytes, the last data shard zero-padded to receiveSize. Decode: rs=reed_solomon_new(dataShards, parityShards); reed_solomon_decode(rs, packets[], marks[], totalPackets, receiveSize) with marks[i]=1 for missing. Data shards = the RTP-framed bytes of the video payload concatenated; depacketizer strips RTP+NV header and concatenates payloads to rebuild the Annex-B AU.", - "notes": "lumen TODAY: shard_index addresses data [0..K) then recovery [K..K+M) within a block, reconstruct() takes received[] of length K+M with None=lost — STRUCTURALLY THE SAME ordering as GameStream's data-then-parity. VERDICT: lumen's reed-solomon GF(2^8) coder CAN produce moonlight-recoverable shards, because both use the same Vandermonde/Cauchy RS over GF(2^8) with data-first layout. BUT the byte CONTENT of each data shard must be GameStream's RTP-framed packet bytes (not lumen's 40-byte-header packets), and the shard size must be packetSize+RTP, and the parity must be computed over those exact bytes. CAVEAT (unverified at byte level): the specific RS library Moonlight uses (reed-solomon-new / Fec.c, a CM256/Plank-style Cauchy matrix) may use a different generator matrix than the Rust `reed-solomon` crate; parity bytes are only interoperable if the matrices match. This MUST be validated against real Moonlight before trusting it — if matrices differ, lumen must port/match Moonlight's Fec.c matrix exactly (this is the single highest-risk interop item)." + "notes": "punktfunk TODAY: shard_index addresses data [0..K) then recovery [K..K+M) within a block, reconstruct() takes received[] of length K+M with None=lost — STRUCTURALLY THE SAME ordering as GameStream's data-then-parity. VERDICT: punktfunk's reed-solomon GF(2^8) coder CAN produce moonlight-recoverable shards, because both use the same Vandermonde/Cauchy RS over GF(2^8) with data-first layout. BUT the byte CONTENT of each data shard must be GameStream's RTP-framed packet bytes (not punktfunk's 40-byte-header packets), and the shard size must be packetSize+RTP, and the parity must be computed over those exact bytes. CAVEAT (unverified at byte level): the specific RS library Moonlight uses (reed-solomon-new / Fec.c, a CM256/Plank-style Cauchy matrix) may use a different generator matrix than the Rust `reed-solomon` crate; parity bytes are only interoperable if the matrices match. This MUST be validated against real Moonlight before trusting it — if matrices differ, punktfunk must port/match Moonlight's Fec.c matrix exactly (this is the single highest-risk interop item)." }, { "name": "video AES-GCM crypto (DELTA)", "layout": "GameStream video_packet_enc_prefix_t = { u8 iv[12]; u32 frameNumber; u8 tag[16] } prepended to the encrypted payload. IV = 8-byte little-endian per-stream gcm_iv_counter in iv[0..8], iv[11]='V' (0x56), iv[8..11]=0; counter increments per packet. Cipher = AES-128-GCM, key = the GCM key from /launch (riKey). Only the post-RTP/post-NV payload is encrypted; RTP+NV header stay in clear. video_short_frame_header_t (8 bytes, inside the encrypted payload, first packet of frame) = { u8 headerType=0x01; le_u16 frame_processing_latency; u8 frameType (1=P,2=IDR,4=intra-refresh,5=after-ref-invalidation); le_u16 lastPayloadLen; u8 unknown[2] }.", - "notes": "DELTA vs lumen crypto.rs: lumen seals the ENTIRE packet (header+payload) and uses a 4-byte salt + 8-byte big-endian seq nonce with seq as AAD, prefixing an 8-byte seq. GameStream encrypts only payload, uses 8-byte LE counter + 'V' marker (NO AAD), and the prefix carries iv+frameNumber+tag explicitly. lumen's per-direction salt-bit trick is a lumen invention not on the GameStream wire. For P1 wire mode the core needs a SEPARATE gcm path matching this prefix exactly." + "notes": "DELTA vs punktfunk crypto.rs: punktfunk seals the ENTIRE packet (header+payload) and uses a 4-byte salt + 8-byte big-endian seq nonce with seq as AAD, prefixing an 8-byte seq. GameStream encrypts only payload, uses 8-byte LE counter + 'V' marker (NO AAD), and the prefix carries iv+frameNumber+tag explicitly. punktfunk's per-direction salt-bit trick is a punktfunk invention not on the GameStream wire. For P1 wire mode the core needs a SEPARATE gcm path matching this prefix exactly." }, { "name": "ENet control crypto + opcodes (new in host)", "layout": "Encrypted control: NVCTL_ENCRYPTED_PACKET_HEADER { le_u16 encryptedHeaderType=0x0001; le_u16 length; u32 seq } then [16-byte AES-GCM tag][encrypted V2 header + payload]. Cipher AES-128-GCM (Sunshine SS_ENC_CONTROL_V2): 12-byte LE IV = seq in bytes 0-3, bytes 10-11='CC'. Plain header V2 = { u16 type; u16 payloadLength }. Opcodes (Gen7 plain): 0x0305 Start A, 0x0307 Start B, 0x0301 invalidate-ref-frames, 0x0201 loss-stats, 0x0206 input, 0x010b rumble, 0x0100 termination, 0x010e HDR, 0x0302 request-IDR (encrypted gen). Periodic ping {le_u16 len=4; le_u32 ts}.", - "notes": "Entirely absent from lumen. Belongs in lumen-host (ENet via a Rust ENet crate); the AES-128-GCM seal/open of the control payload can reuse a core crypto primitive but the framing is host-side." + "notes": "Entirely absent from punktfunk. Belongs in punktfunk-host (ENet via a Rust ENet crate); the AES-128-GCM seal/open of the control payload can reuse a core crypto primitive but the framing is host-side." }, { "name": "audio packet (new in host)", @@ -402,37 +402,37 @@ } ], "flow": [ - "PHASE A (core, low risk): Add a P1 'gamestream wire mode' to lumen-core alongside the internal format. New module crates/lumen-core/src/protocol/gamestream.rs implementing (a) RTP+reserved+NV_VIDEO_PACKET serialize/parse with exact bit-packing, (b) a GameStream-layout FEC packetizer/reassembler that emits data-then-parity contiguous RTP shards at packetSize+RTP shard size, (c) the video_packet_enc_prefix_t AES-128-GCM path. Gate behind ProtocolPhase::P1GameStream (already exists). Keep lumen's internal 40-byte format for P2.", - "PHASE B (validate the FEC matrix — HIGHEST RISK, do early): Before building any host networking, prove byte-for-byte that lumen's reed-solomon GF(2^8) parity matches Moonlight's expectation. Capture real Sunshine video packets (or vendor moonlight-common-c's Fec.c into a test) and assert lumen-encoded parity is decodable by Moonlight's RS and vice versa. If the generator matrices differ, port Moonlight's Cauchy matrix into lumen's gf8 coder. This gates everything: if shards aren't interoperable, P1 is dead.", - "PHASE C (host control plane, in lumen-host): Implement nvhttp on TCP 47989 (HTTP) + 47984 (HTTPS): /serverinfo XML (appversion, GfeVersion, uniqueid, HttpsPort, ExternalPort, mac, MaxLumaPixelsHEVC, ServerCodecModeSupport, currentgame, PairStatus, sessionUrl0), the /pair PIN state machine (getservercert -> clientchallenge -> serverchallengeresp -> clientpairingsecret) with PIN-AES = SHA-256(salt||pin)[..16], AES-128-ECB challenge, SHA-256, X.509 + RSA sign/verify. Persist the paired client cert; pin it for HTTPS client-cert auth.", - "PHASE D (RTSP on TCP 48010): OPTIONS/DESCRIBE/SETUP/ANNOUNCE/PLAY. DESCRIBE returns SDP with x-nv-video[0].* , x-nv-vqos[0].fec.* , x-ss-general.* attributes. SETUP returns server_port= per stream. ANNOUNCE parses client's packetSize, fec.minRequiredFecPackets, maximumBitrateKbps, videoEncoderSlicesPerFrame — feed these into the lumen-core Config (shard_payload=packetSize, fec_percent, etc).", - "PHASE E (data plane wiring): On PLAY, bind UDP 47998 (video), spawn the M0 capture->NVENC pipeline, and drive lumen-core's P1 packetizer to that socket. Bind UDP 48000 ENet control (request-IDR -> force NVENC keyframe; loss-stats -> adjust; termination). Audio (UDP 47999, AES-CBC) and full input can follow.", + "PHASE A (core, low risk): Add a P1 'gamestream wire mode' to punktfunk-core alongside the internal format. New module crates/punktfunk-core/src/protocol/gamestream.rs implementing (a) RTP+reserved+NV_VIDEO_PACKET serialize/parse with exact bit-packing, (b) a GameStream-layout FEC packetizer/reassembler that emits data-then-parity contiguous RTP shards at packetSize+RTP shard size, (c) the video_packet_enc_prefix_t AES-128-GCM path. Gate behind ProtocolPhase::P1GameStream (already exists). Keep punktfunk's internal 40-byte format for P2.", + "PHASE B (validate the FEC matrix — HIGHEST RISK, do early): Before building any host networking, prove byte-for-byte that punktfunk's reed-solomon GF(2^8) parity matches Moonlight's expectation. Capture real Sunshine video packets (or vendor moonlight-common-c's Fec.c into a test) and assert punktfunk-encoded parity is decodable by Moonlight's RS and vice versa. If the generator matrices differ, port Moonlight's Cauchy matrix into punktfunk's gf8 coder. This gates everything: if shards aren't interoperable, P1 is dead.", + "PHASE C (host control plane, in punktfunk-host): Implement nvhttp on TCP 47989 (HTTP) + 47984 (HTTPS): /serverinfo XML (appversion, GfeVersion, uniqueid, HttpsPort, ExternalPort, mac, MaxLumaPixelsHEVC, ServerCodecModeSupport, currentgame, PairStatus, sessionUrl0), the /pair PIN state machine (getservercert -> clientchallenge -> serverchallengeresp -> clientpairingsecret) with PIN-AES = SHA-256(salt||pin)[..16], AES-128-ECB challenge, SHA-256, X.509 + RSA sign/verify. Persist the paired client cert; pin it for HTTPS client-cert auth.", + "PHASE D (RTSP on TCP 48010): OPTIONS/DESCRIBE/SETUP/ANNOUNCE/PLAY. DESCRIBE returns SDP with x-nv-video[0].* , x-nv-vqos[0].fec.* , x-ss-general.* attributes. SETUP returns server_port= per stream. ANNOUNCE parses client's packetSize, fec.minRequiredFecPackets, maximumBitrateKbps, videoEncoderSlicesPerFrame — feed these into the punktfunk-core Config (shard_payload=packetSize, fec_percent, etc).", + "PHASE E (data plane wiring): On PLAY, bind UDP 47998 (video), spawn the M0 capture->NVENC pipeline, and drive punktfunk-core's P1 packetizer to that socket. Bind UDP 48000 ENet control (request-IDR -> force NVENC keyframe; loss-stats -> adjust; termination). Audio (UDP 47999, AES-CBC) and full input can follow.", "PHASE F (discovery + display): mDNS-advertise _nvstream._tcp. On RTSP SETUP/PLAY, create the wlroots virtual output sized to the negotiated WxH@fps, point M0 capture at it, tear down on RTSP TEARDOWN / ENet termination." ], - "crypto": "VIDEO (P1 wire): AES-128-GCM, key=riKey from /launch (16 bytes). Per-packet prefix video_packet_enc_prefix_t{iv[12],u32 frameNumber,u8 tag[16]}; IV = 8-byte LE per-stream counter in iv[0..8], iv[11]='V'(0x56), no AAD. Only payload encrypted. ||| CONTROL: AES-128-GCM (Sunshine SS_ENC_CONTROL_V2), 12-byte LE IV = seq[0..4], iv[10..12]='CC', 16-byte tag, NVCTL_ENCRYPTED_PACKET_HEADER prefix. ||| AUDIO: AES-128-CBC, IV = BE u32(avRiKeyId + seq), avRiKeyId = first 4 bytes of launch IV. ||| PAIRING (nvhttp): PIN-derived key = first 16 bytes of SHA-256(salt(16) || ascii-pin(4)); AES-128-ECB for the challenge/response blocks; SHA-256 for the rolling hashes; RSA (server key + client cert) for signing/verifying the pairing secret; X.509 certs exchanged (server cert returned in getservercert, client cert pinned for HTTPS). ||| DELTA vs lumen crypto.rs: lumen uses AES-128-GCM but with a 4-byte random salt + 8-byte BE seq nonce and seq-as-AAD, sealing the WHOLE packet and prefixing 8-byte seq — none of these match GameStream's iv/marker/prefix/no-AAD scheme. lumen has NO ECB/CBC, NO RSA/X.509, NO PIN-KDF. So: keep lumen's GCM for P2; add a distinct gamestream-gcm path for P1; add ECB+CBC+RSA+X.509+SHA-256-KDF in the host pairing layer (rustls/aws-lc-rs/rsa/x509 crates).", - "rust_options": "FEC: KEEP the existing `reed-solomon` GF(2^8) coder in lumen-core for math, but it MUST be validated byte-compatible with Moonlight's Fec.c (CM256/Plank Cauchy matrix) — if not, port that matrix. (reed-solomon-simd is GF(2^16), P2 only, NOT moonlight-compatible.) ||| ENet control: `rusty_enet` (pure-Rust ENet 1.3.x, no_std-friendly, actively maintained) — speaks the exact ENet wire protocol Moonlight expects; alternative is FFI to libenet via `enet-sys`. ||| RTSP: NO good off-the-shelf server crate handles GameStream's non-standard interleaved/encrypted RTSP — hand-roll a minimal parser over a tokio TcpListener (it's ~6 verbs); `httparse`-style manual parsing. Do NOT pull a full RTSP stack. ||| HTTPS with pinned client-cert: `axum`/`hyper` + `rustls` (ServerConfig with a custom `ClientCertVerifier` that checks the cert against the paired set) + `tokio-rustls`; or `actix-web` with rustls. The plan already commits to axum+tokio for the control plane. ||| X.509 gen: `rcgen` (generate the self-signed server cert + key on first run); parse/verify client certs with `x509-parser` + `rsa` + `sha2`. PIN-KDF and ECB/CBC/GCM via `aes`, `aes-gcm`, `cbc`, `ecb` (RustCrypto) or `aws-lc-rs`/`openssl`. ||| mDNS: `mdns-sd` (pure-Rust, registers `_nvstream._tcp.local` with TXT records) or `zeroconf` (FFI to Avahi). `mdns-sd` preferred (no daemon dependency). ||| Opus audio: `audiopus`/`opus` crate if/when audio is implemented.", - "reuse_from_lumen": "REUSE: (1) lumen-core's GF(2^8) `reed-solomon` coder and its data-then-parity reconstruct() contract — same ordering as GameStream; (2) FecConfig::recovery_for ceil(k*pct/100) — IDENTICAL to Moonlight's parity math; (3) the ReassemblerLimits bounds-before-allocate hardening pattern — reuse the same discipline when parsing attacker-controlled NV_VIDEO_PACKET fields; (4) aes-gcm dependency and crypto.rs structure (the GCM primitive itself, even though nonce/prefix scheme differs); (5) ProtocolPhase::P1GameStream / FecScheme::Gf8 enums already exist as the negotiation hook; (6) lumen-host M0's capture->NVENC pipeline produces exactly the HEVC/H264 Annex-B AUs that become GameStream video payload; (7) the Packetizer/Reassembler split is the right shape — add a parallel GameStream packetizer/reassembler beside them. ||| MUST BUILD NEW: the RTP+NV_VIDEO_PACKET (de)serialization with bit-packed fecInfo/multiFecBlocks; the GameStream-layout shard emitter (contiguous data-then-parity, packetSize+RTP shard size, no 40-byte lumen header); the video_packet_enc_prefix_t GCM path (iv counter + 'V', payload-only, no AAD); the ENTIRE control plane (nvhttp pairing, RTSP, ENet control, mDNS, X.509/RSA, ECB/CBC); audio AES-CBC path. ||| CANNOT REUSE on the wire: lumen's 40-byte PacketHeader, its 8-byte-seq GCM framing, its per-direction salt bit — all are lumen-internal inventions absent from GameStream.", + "crypto": "VIDEO (P1 wire): AES-128-GCM, key=riKey from /launch (16 bytes). Per-packet prefix video_packet_enc_prefix_t{iv[12],u32 frameNumber,u8 tag[16]}; IV = 8-byte LE per-stream counter in iv[0..8], iv[11]='V'(0x56), no AAD. Only payload encrypted. ||| CONTROL: AES-128-GCM (Sunshine SS_ENC_CONTROL_V2), 12-byte LE IV = seq[0..4], iv[10..12]='CC', 16-byte tag, NVCTL_ENCRYPTED_PACKET_HEADER prefix. ||| AUDIO: AES-128-CBC, IV = BE u32(avRiKeyId + seq), avRiKeyId = first 4 bytes of launch IV. ||| PAIRING (nvhttp): PIN-derived key = first 16 bytes of SHA-256(salt(16) || ascii-pin(4)); AES-128-ECB for the challenge/response blocks; SHA-256 for the rolling hashes; RSA (server key + client cert) for signing/verifying the pairing secret; X.509 certs exchanged (server cert returned in getservercert, client cert pinned for HTTPS). ||| DELTA vs punktfunk crypto.rs: punktfunk uses AES-128-GCM but with a 4-byte random salt + 8-byte BE seq nonce and seq-as-AAD, sealing the WHOLE packet and prefixing 8-byte seq — none of these match GameStream's iv/marker/prefix/no-AAD scheme. punktfunk has NO ECB/CBC, NO RSA/X.509, NO PIN-KDF. So: keep punktfunk's GCM for P2; add a distinct gamestream-gcm path for P1; add ECB+CBC+RSA+X.509+SHA-256-KDF in the host pairing layer (rustls/aws-lc-rs/rsa/x509 crates).", + "rust_options": "FEC: KEEP the existing `reed-solomon` GF(2^8) coder in punktfunk-core for math, but it MUST be validated byte-compatible with Moonlight's Fec.c (CM256/Plank Cauchy matrix) — if not, port that matrix. (reed-solomon-simd is GF(2^16), P2 only, NOT moonlight-compatible.) ||| ENet control: `rusty_enet` (pure-Rust ENet 1.3.x, no_std-friendly, actively maintained) — speaks the exact ENet wire protocol Moonlight expects; alternative is FFI to libenet via `enet-sys`. ||| RTSP: NO good off-the-shelf server crate handles GameStream's non-standard interleaved/encrypted RTSP — hand-roll a minimal parser over a tokio TcpListener (it's ~6 verbs); `httparse`-style manual parsing. Do NOT pull a full RTSP stack. ||| HTTPS with pinned client-cert: `axum`/`hyper` + `rustls` (ServerConfig with a custom `ClientCertVerifier` that checks the cert against the paired set) + `tokio-rustls`; or `actix-web` with rustls. The plan already commits to axum+tokio for the control plane. ||| X.509 gen: `rcgen` (generate the self-signed server cert + key on first run); parse/verify client certs with `x509-parser` + `rsa` + `sha2`. PIN-KDF and ECB/CBC/GCM via `aes`, `aes-gcm`, `cbc`, `ecb` (RustCrypto) or `aws-lc-rs`/`openssl`. ||| mDNS: `mdns-sd` (pure-Rust, registers `_nvstream._tcp.local` with TXT records) or `zeroconf` (FFI to Avahi). `mdns-sd` preferred (no daemon dependency). ||| Opus audio: `audiopus`/`opus` crate if/when audio is implemented.", + "reuse_from_punktfunk": "REUSE: (1) punktfunk-core's GF(2^8) `reed-solomon` coder and its data-then-parity reconstruct() contract — same ordering as GameStream; (2) FecConfig::recovery_for ceil(k*pct/100) — IDENTICAL to Moonlight's parity math; (3) the ReassemblerLimits bounds-before-allocate hardening pattern — reuse the same discipline when parsing attacker-controlled NV_VIDEO_PACKET fields; (4) aes-gcm dependency and crypto.rs structure (the GCM primitive itself, even though nonce/prefix scheme differs); (5) ProtocolPhase::P1GameStream / FecScheme::Gf8 enums already exist as the negotiation hook; (6) punktfunk-host M0's capture->NVENC pipeline produces exactly the HEVC/H264 Annex-B AUs that become GameStream video payload; (7) the Packetizer/Reassembler split is the right shape — add a parallel GameStream packetizer/reassembler beside them. ||| MUST BUILD NEW: the RTP+NV_VIDEO_PACKET (de)serialization with bit-packed fecInfo/multiFecBlocks; the GameStream-layout shard emitter (contiguous data-then-parity, packetSize+RTP shard size, no 40-byte punktfunk header); the video_packet_enc_prefix_t GCM path (iv counter + 'V', payload-only, no AAD); the ENTIRE control plane (nvhttp pairing, RTSP, ENet control, mDNS, X.509/RSA, ECB/CBC); audio AES-CBC path. ||| CANNOT REUSE on the wire: punktfunk's 40-byte PacketHeader, its 8-byte-seq GCM framing, its per-direction salt bit — all are punktfunk-internal inventions absent from GameStream.", "gotchas": [ "RS GENERATOR MATRIX is the #1 interop risk: same GF(2^8) RS and same data-first ordering does NOT guarantee byte-compatible parity. Moonlight's Fec.c uses a specific Cauchy/Vandermonde matrix; the Rust `reed-solomon` crate may differ. Validate against real Moonlight FIRST (Phase B) or all of P1 fails silently as 'unrecoverable loss'.", - "GameStream RTP header is BIG-endian; lumen's PacketHeader is little-endian. NV_VIDEO_PACKET itself is little-endian. Don't conflate them.", - "streamPacketIndex is a per-STREAM monotonic packet counter (the RTP-ish sequence), NOT lumen's per-AU stream_seq. frameIndex is the per-frame counter. Two different counters.", + "GameStream RTP header is BIG-endian; punktfunk's PacketHeader is little-endian. NV_VIDEO_PACKET itself is little-endian. Don't conflate them.", + "streamPacketIndex is a per-STREAM monotonic packet counter (the RTP-ish sequence), NOT punktfunk's per-AU stream_seq. frameIndex is the per-frame counter. Two different counters.", "multiFecBlocks caps a frame at 4 FEC blocks (2 bits). Combined with the 255-shard GF(2^8) cap, large frames at high res can overflow — this IS the 1 Gbps wall the plan describes. P1 must keep frames within 4 blocks x 255 shards; reduce via slicesPerFrame / bitrate from ANNOUNCE.", - "Video GCM uses NO AAD and an 8-byte LE counter + 'V' marker; lumen's GCM uses seq-as-AAD + per-direction salt. A naive reuse of lumen's seal() will produce undecryptable-by-Moonlight packets. Build the gamestream-gcm path separately.", + "Video GCM uses NO AAD and an 8-byte LE counter + 'V' marker; punktfunk's GCM uses seq-as-AAD + per-direction salt. A naive reuse of punktfunk's seal() will produce undecryptable-by-Moonlight packets. Build the gamestream-gcm path separately.", "Encryption is NEGOTIATED (ENCFLG_VIDEO=0x2, ENCFLG_AUDIO=0x1 in serverinfo/SETUP). Many Moonlight setups stream video in the CLEAR on LAN — implement plaintext video first, add GCM second; serverinfo's encryptionSupported/Requested controls this.", "Audio is AES-CBC not GCM, with a BE counter IV — a third distinct crypto scheme. Easy to get wrong if you assume GCM everywhere.", "Pairing PIN key = SHA-256(salt||pin)[..16] where pin is the 4 ASCII digits and salt is 16 raw bytes from the client — order and encoding matter exactly. ECB (not CBC) for the challenge blocks.", - "The shard size is packetSize + RTP_HEADER (not just packetSize). lumen's shard_payload must be set to the negotiated packetSize and the shard the core FEC-protects must include the RTP/NV framing bytes, else the depacketizer mis-aligns.", + "The shard size is packetSize + RTP_HEADER (not just packetSize). punktfunk's shard_payload must be set to the negotiated packetSize and the shard the core FEC-protects must include the RTP/NV framing bytes, else the depacketizer mis-aligns.", "HTTPS endpoint pins the CLIENT cert obtained during pairing; a stock TLS server that accepts any cert will let unpaired clients in. Use a custom rustls ClientCertVerifier.", "mDNS service name must be exactly _nvstream._tcp with the right TXT records or Moonlight won't auto-discover (manual IP add still works as a fallback for testing)." ], "sources": [ - "/home/enricobuehler/lumen/crates/lumen-core/src/packet.rs (PacketHeader 40-byte layout, Packetizer/Reassembler, ReassemblerLimits hardening, FLAG_* constants)", - "/home/enricobuehler/lumen/crates/lumen-core/src/crypto.rs (SessionCrypto AES-128-GCM, 4-byte salt + 8-byte BE seq nonce, seq-as-AAD, per-direction salt bit)", - "/home/enricobuehler/lumen/crates/lumen-core/src/config.rs (FecConfig::recovery_for ceil(k*pct/100), FecScheme::max_total_shards Gf8=255, ProtocolPhase::P1GameStream, p1_defaults)", - "/home/enricobuehler/lumen/crates/lumen-core/src/session.rs (seal_for_wire 8-byte seq prefix, submit_frame/poll_frame hot path)", - "/home/enricobuehler/lumen/crates/lumen-core/src/fec/mod.rs (ErasureCoder trait, data-then-parity reconstruct contract, GF(2^8) Gf8Coder)", - "/home/enricobuehler/lumen/crates/lumen-host/src/{web.rs,vdisplay.rs,inject.rs,pipeline.rs,m0.rs} (control-plane stub, VirtualDisplay trait + wlroots/kwin/mutter stubs, M0 capture->NVENC->AU pipeline + lumen-core loopback)", - "/home/enricobuehler/lumen/docs/implementation-plan.md sections 3,5,6,8 (P1/P2/P3 strategy, C ABI, virtual-display orchestration, milestones M0/M2)", + "/home/enricobuehler/punktfunk/crates/punktfunk-core/src/packet.rs (PacketHeader 40-byte layout, Packetizer/Reassembler, ReassemblerLimits hardening, FLAG_* constants)", + "/home/enricobuehler/punktfunk/crates/punktfunk-core/src/crypto.rs (SessionCrypto AES-128-GCM, 4-byte salt + 8-byte BE seq nonce, seq-as-AAD, per-direction salt bit)", + "/home/enricobuehler/punktfunk/crates/punktfunk-core/src/config.rs (FecConfig::recovery_for ceil(k*pct/100), FecScheme::max_total_shards Gf8=255, ProtocolPhase::P1GameStream, p1_defaults)", + "/home/enricobuehler/punktfunk/crates/punktfunk-core/src/session.rs (seal_for_wire 8-byte seq prefix, submit_frame/poll_frame hot path)", + "/home/enricobuehler/punktfunk/crates/punktfunk-core/src/fec/mod.rs (ErasureCoder trait, data-then-parity reconstruct contract, GF(2^8) Gf8Coder)", + "/home/enricobuehler/punktfunk/crates/punktfunk-host/src/{web.rs,vdisplay.rs,inject.rs,pipeline.rs,m0.rs} (control-plane stub, VirtualDisplay trait + wlroots/kwin/mutter stubs, M0 capture->NVENC->AU pipeline + punktfunk-core loopback)", + "/home/enricobuehler/punktfunk/docs/implementation-plan.md sections 3,5,6,8 (P1/P2/P3 strategy, C ABI, virtual-display orchestration, milestones M0/M2)", "moonlight-common-c/src/Video.h (NV_VIDEO_PACKET 16-byte struct, RTP_PACKET 12-byte struct, FLAG_CONTAINS_PIC_DATA/EOF/SOF, FIXED_RTP_HEADER_SIZE)", "moonlight-common-c/src/RtpVideoQueue.c (fecInfo masks 0xFFC00000>>22 / 0x3FF000>>12 / 0xFF0>>4, parity=(data*pct+99)/100, reed_solomon_new/reed_solomon_decode, receiveSize=packetSize+MAX_RTP_HEADER_SIZE, contiguous data-then-parity sequence range, multiFecBlocks>>4&0x3 / >>6&0x3)", "moonlight-common-c/src/ControlStream.c (ENet channels, opcodes 0x0305/0x0307/0x0301/0x0201/0x010b/0x0100/0x010e/0x0302, NVCTL_ENCRYPTED_PACKET_HEADER, AES-128-GCM control IV seq+'CC', ping/loss-stats/HDR/rumble/termination layouts)", 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 b8a5f4c..96a6c37 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 d68b4b7..ca99b52 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 3111600..8951ad0 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 fef7eb0..8dff1af 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 32fe822..cb2839d 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 36f3306..ba7a25c 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 434c9a0..411a8e3 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 2b9c28c..12c0559 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 753910b..37e2076 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 0f21043..5a0adf3 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 754358f..0000000 --- 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 0000000..f168d3d --- /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 fc8d7fd..3c8e034 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 cac7a71..d46ffcb 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}",