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

Full project rename, decided 2026-06-10:
- Crates/binaries: punktfunk-core / punktfunk-host / punktfunk-client-rs.
- C ABI: punktfunk_* symbols, Punktfunk* types, include/punktfunk_core.h,
  PUNKTFUNK_FEATURE_QUIC guard (header regenerated; cbindgen renames updated, incl.
  PUNKTFUNK_BTN_*/PUNKTFUNK_AXIS_* wire constants).
- Protocol: punktfunk/1 — control-plane magic LMN1 → PKF1, nonce salt lmn1 → pkf1.
  WIRE BREAK: clients must be rebuilt from this revision.
- Env knobs: PUNKTFUNK_VIDEO_SOURCE / PUNKTFUNK_COMPOSITOR / PUNKTFUNK_ZEROCOPY / ….
- Host config dir: ~/.config/punktfunk (the box's dir was migrated in place — the
  persistent identity is unchanged, pinned fingerprints stay valid).
- Swift package: PunktfunkKit + PunktfunkCore.xcframework + PunktfunkConnection
  (Sources/PunktfunkClient app + tests renamed with it); build-xcframework.sh updated.
- scripts/: 60-punktfunk.rules, punktfunk-host.service; OpenAPI doc regenerated.

Also: scripts/headless/run-headless-kde.sh — full headless Plasma bringup. Root cause of
"desktop but no apps/settings" over the stream: plasmashell launched without
XDG_MENU_PREFIX=plasma-, so the launcher resolved a nonexistent applications.menu and
rendered an empty menu. The script sets the complete KDE session env (menu prefix,
KDE_FULL_SESSION, session version) and rebuilds ksycoca before starting plasmashell.

Gate: 97/97 tests, clippy -D warnings (both feature sets), fmt, C-ABI harness PASS,
zero lumen references left outside .git.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 13:11:59 +00:00
parent b8b23c8fb2
commit bfd64ce871
119 changed files with 1245 additions and 1185 deletions
+5 -5
View File
@@ -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. # Adjust `runs-on` to match your runner labels if not using the default ubuntu image.
name: ci name: ci
@@ -34,10 +34,10 @@ jobs:
run: cargo test --workspace --locked run: cargo test --workspace --locked
- name: C ABI harness (standalone link proof) - 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 - name: Verify generated header is committed & up to date
run: | run: |
cargo build -p lumen-core cargo build -p punktfunk-core
git diff --exit-code include/lumen_core.h \ git diff --exit-code include/punktfunk_core.h \
|| (echo "include/lumen_core.h is stale — commit the regenerated header" && exit 1) || (echo "include/punktfunk_core.h is stale — commit the regenerated header" && exit 1)
+1 -1
View File
@@ -9,5 +9,5 @@ node_modules/
dist/ dist/
# Swift package build artifacts + the locally-built xcframework (rebuild via scripts/build-xcframework.sh) # Swift package build artifacts + the locally-built xcframework (rebuild via scripts/build-xcframework.sh)
clients/apple/.build/ clients/apple/.build/
clients/apple/LumenCore.xcframework/ clients/apple/PunktfunkCore.xcframework/
clients/apple/.swiftpm/ clients/apple/.swiftpm/
+39 -40
View File
@@ -1,17 +1,17 @@
# CLAUDE.md — lumen # CLAUDE.md — punktfunk
Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protocol core 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`. [`docs/implementation-plan.md`](docs/implementation-plan.md). Status table: `README.md`.
## Where the work stands ## 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 + proptests, C ABI harness all green; 13 adversarial-review findings fixed +
regression-tested (`a913042`). regression-tested (`a913042`).
- **M2 (GameStream host): working end-to-end with a stock Moonlight client.** Validated live - **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 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 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: 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 **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 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 + back-channel; validated live — pad created/destroyed with the session). Management REST API +
checked-in OpenAPI doc (`mgmt.rs`). checked-in OpenAPI doc (`mgmt.rs`).
- **M3 (`lumen/1`, the native protocol): full session planes, validated live.** QUIC - **M3 (`punktfunk/1`, the native protocol): full session planes, validated live.** QUIC
control plane (`lumen-core` `quic` feature: Hello{mode}/Welcome{full Config}/Start), data 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** 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 (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; 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 `--max-sessions`). QUIC datagrams carry the side planes, demuxed by first byte: input
0xC8 (incl. **gamepads** — incremental events accumulated into the uinput xpad), **Opus 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:** 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 — 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** `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). 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`. (pin/TOFU) + `next_au`/`next_audio`/`next_rumble`/`send_input`.
## What's left ## What's left
1. **M4 — client decode + present: macOS stage 1 done, first light achieved 1. **M4 — client decode + present: macOS stage 1 done, first light achieved
(2026-06-10).** LumenKit compiles and is tested on macOS (AnnexB → VideoToolbox → (2026-06-10).** PunktfunkKit compiles and is tested on macOS (AnnexB → VideoToolbox →
`AVSampleBufferDisplayLayer`, GCMouse/GCKeyboard capture, `LumenClient` app shell); `AVSampleBufferDisplayLayer`, GCMouse/GCKeyboard capture, `PunktfunkClient` app shell);
validated live Mac ↔ this box at 720p60 — vkcube on glass, input injected via gamescope 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), 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), `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 [`clients/apple/README.md`](clients/apple/README.md). Next: stage 2 presenter
(`VTDecompressionSession` + `CAMetalLayer` frame pacing), glass-to-glass numbers via (`VTDecompressionSession` + `CAMetalLayer` frame pacing), glass-to-glass numbers via
`tools/latency-probe` (scaffold), iOS variant. The Linux reference client `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 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 (~24 ms NVENC SDK wrapper (libavcodec only emits whole AUs) — the next big latency lever (~24 ms
at high res). 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 mid-stream mode renegotiation (the Welcome is one-shot today), concurrent sessions
(today: one at a time, extras wait in the accept queue). (today: one at a time, extras wait in the accept queue).
4. **M2 polish**: wlroots/Sway `VirtualDisplay` backend (deferred; swaymsg `create_output`), 4. **M2 polish**: wlroots/Sway `VirtualDisplay` backend (deferred; swaymsg `create_output`),
HDR/10-bit/AV1 negotiation, surround audio, reconnect-at-new-mode robustness. 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), 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 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 fmt --all --check
cargo run -p loss-harness # FEC loss-resilience sweep (no network needed) 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` Generated artifacts are **checked in** and CI fails on drift: `include/punktfunk_core.h`
(cbindgen from `lumen-core/src/abi.rs`) and `docs/api/openapi.json` (regenerate with (cbindgen from `punktfunk-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`). `cargo run -p punktfunk-host -- openapi > docs/api/openapi.json`; spec lives in `mgmt.rs`).
## Layout ## Layout
``` ```
crates/lumen-core/ protocol · FEC · crypto · quic (lumen/1 control plane, feature-gated) crates/punktfunk-core/ protocol · FEC · crypto · quic (punktfunk/1 control plane, feature-gated)
crates/lumen-host/ crates/punktfunk-host/
gamestream/ Moonlight compat: nvhttp · pairing · rtsp · control · stream · gamepad · apps gamestream/ Moonlight compat: nvhttp · pairing · rtsp · control · stream · gamepad · apps
vdisplay/{kwin,gamescope,mutter}.rs per-compositor client-sized virtual outputs 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) 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) inject/{libei,wlr,gamepad}.rs input backends (+ uinput virtual gamepads)
capture.rs · encode.rs · audio.rs · m0.rs · m3.rs · mgmt.rs 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) tools/{loss-harness,latency-probe}/ measurement (plan §10)
scripts/ 60-lumen.rules · lumen-host.service · host.env.example · headless/ scripts/ 60-punktfunk.rules · punktfunk-host.service · host.env.example · headless/
include/lumen_core.h generated C header include/punktfunk_core.h generated C header
``` ```
## Design invariants — do not regress ## 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 stable, versioned C ABI. `tokio`/`quinn` exist only behind the `quic` feature (control
plane); **no async on the per-frame path** — native threads only. 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 - **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 remote_fd, preferred_mode, keepalive }`, RAII teardown). There is no cross-compositor
protocol for this — each compositor keeps its own backend. protocol for this — each compositor keeps its own backend.
- **FEC is the wall-breaker.** GF(2⁸) (≤255 shards/block, Moonlight-compatible) and GF(2¹⁶) - **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. ceiling.
- **M1 security hardening stays intact**: reassembler bounds attacker-controlled fields - **M1 security hardening stays intact**: reassembler bounds attacker-controlled fields
before allocating (`ReassemblerLimits`); AES-GCM per-direction nonce salts + seq-as-AAD; 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`. scanout → KWin `--drm` impossible; everything renders offscreen via `renderD128`.
```sh ```sh
# compositor session (shell 1, or the systemd unit in scripts/): # compositor session (shell 1, or the systemd unit in scripts/): full headless Plasma.
XDG_RUNTIME_DIR=/run/user/1000 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus \ # The script sets XDG_MENU_PREFIX=plasma- & co. — without it plasmashell runs but the
XDG_CURRENT_DESKTOP=KDE KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 \ # launcher menu is EMPTY (no apps, no System Settings).
kwin_wayland --virtual --width 1920 --height 1080 --no-lockscreen --socket wayland-kde \ bash scripts/headless/run-headless-kde.sh 1920x1080
--exit-with-session wev
# host (shell 2): # host (shell 2):
WAYLAND_DISPLAY=wayland-kde XDG_CURRENT_DESKTOP=KDE LUMEN_VIDEO_SOURCE=virtual \ WAYLAND_DISPLAY=wayland-kde XDG_CURRENT_DESKTOP=KDE PUNKTFUNK_VIDEO_SOURCE=virtual \
LUMEN_ZEROCOPY=1 cargo run -rp lumen-host -- serve 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): # across sessions — bound it with --max-sessions):
cargo run -rp lumen-host -- m3-host --source virtual --seconds 10 --max-sessions 1 cargo run -rp punktfunk-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-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 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`, (system FFmpeg 8 / libavcodec 62). Env knobs: `PUNKTFUNK_VIDEO_SOURCE=virtual|portal`,
`LUMEN_COMPOSITOR=kwin|gamescope|mutter`, `LUMEN_ZEROCOPY=1`, `LUMEN_GAMESCOPE_APP=...`, `PUNKTFUNK_COMPOSITOR=kwin|gamescope|mutter`, `PUNKTFUNK_ZEROCOPY=1`, `PUNKTFUNK_GAMESCOPE_APP=...`,
`LUMEN_INPUT_BACKEND=...`, `LUMEN_PERF=1` (per-stage timing), `LUMEN_VIDEO_DROP=N` (FEC `PUNKTFUNK_INPUT_BACKEND=...`, `PUNKTFUNK_PERF=1` (per-stage timing), `PUNKTFUNK_VIDEO_DROP=N` (FEC
test), `LUMEN_FEC_PCT=N`. test), `PUNKTFUNK_FEC_PCT=N`.
## Conventions ## Conventions
@@ -155,4 +154,4 @@ test), `LUMEN_FEC_PCT=N`.
- Match the surrounding code's comment density and naming. - Match the surrounding code's comment density and naming.
- Commit messages end with the Co-Authored-By trailer (see `git log`). - 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` 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.
Generated
+85 -85
View File
@@ -1451,7 +1451,7 @@ checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
name = "loss-harness" name = "loss-harness"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"lumen-core", "punktfunk-core",
] ]
[[package]] [[package]]
@@ -1460,90 +1460,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 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]] [[package]]
name = "matchers" name = "matchers"
version = "0.2.0" version = "0.2.0"
@@ -1981,6 +1897,90 @@ dependencies = [
"unarray", "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]] [[package]]
name = "quick-error" name = "quick-error"
version = "1.2.3" version = "1.2.3"
+6 -6
View File
@@ -1,9 +1,9 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"crates/lumen-core", "crates/punktfunk-core",
"crates/lumen-host", "crates/punktfunk-host",
"crates/lumen-client-rs", "crates/punktfunk-client-rs",
"tools/latency-probe", "tools/latency-probe",
"tools/loss-harness", "tools/loss-harness",
] ]
@@ -14,15 +14,15 @@ edition = "2021"
rust-version = "1.82" rust-version = "1.82"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
authors = ["unom"] authors = ["unom"]
repository = "https://git.unom.io/unom/lumen" repository = "https://git.unom.io/unom/punktfunk"
[profile.release] [profile.release]
opt-level = 3 opt-level = 3
lto = "thin" lto = "thin"
codegen-units = 1 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 # 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 # no-op and let a stray panic abort the embedding application. Unwinding keeps the
# documented isolation guarantee real. # documented isolation guarantee real.
+19 -19
View File
@@ -1,9 +1,9 @@
# lumen # punktfunk
*A ground-up low-latency desktop streaming stack, built Linux-first, with a shared Rust *A ground-up low-latency desktop streaming stack, built Linux-first, with a shared Rust
protocol core and native clients per platform.* 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 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 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). 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 | | Milestone | State |
|-----------|-------| |-----------|-------|
| **M1 — `lumen-core` + C ABI** | ✅ done & tested (FEC, packetization, crypto, session, `lumen_core.h`) | | **M1 — `punktfunk-core` + C ABI** | ✅ done & tested (FEC, packetization, crypto, session, `punktfunk_core.h`) |
| **M0 — pipeline spike** (wlroots→PipeWire→NVENC→file→`lumen-core`) | ✅ done & verified on NVIDIA (RTX 5070 Ti / driver 595) | | **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 | | M2 — P1 host → stock Moonlight | 🟡 capture+encode landed in M0; pairing/RTSP/vdisplay pending |
| M3 — measurement harness | 🟡 `tools/loss-harness` runs; `latency-probe` scaffolded | | M3 — measurement harness | 🟡 `tools/loss-harness` runs; `latency-probe` scaffolded |
| M4 — P2 transport + Rust client | 🟡 GF(2¹⁶) core done; `lumen-client-rs` scaffolded | | M4 — P2 transport + Rust client | 🟡 GF(2¹⁶) core done; `punktfunk-client-rs` scaffolded |
| M5 — Apple client | 🟡 macOS first light: HEVC on glass + input over `lumen/1` (`clients/apple`) | | 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 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 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 `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 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 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/ crates/
lumen-core/ protocol · FEC · pacing · crypto — the C ABI (lib + cdylib + staticlib) punktfunk-core/ protocol · FEC · pacing · crypto — the C ABI (lib + cdylib + staticlib)
lumen-host/ Linux host: vdisplay · capture · encode · inject · gamestream · mgmt punktfunk-host/ Linux host: vdisplay · capture · encode · inject · gamestream · mgmt
lumen-client-rs/ reference client (M4): VAAPI decode + wgpu present punktfunk-client-rs/ reference client (M4): VAAPI decode + wgpu present
clients/{apple,android}/ native client scaffolds (import lumen_core.h) clients/{apple,android}/ native client scaffolds (import punktfunk_core.h)
include/lumen_core.h cbindgen-generated C header (checked in) include/punktfunk_core.h cbindgen-generated C header (checked in)
tools/{latency-probe,loss-harness}/ measurement (plan §10) tools/{latency-probe,loss-harness}/ measurement (plan §10)
docs/implementation-plan.md docs/implementation-plan.md
``` ```
@@ -50,16 +50,16 @@ cargo test --workspace # unit + loopback + proptest + C ABI harness
cargo clippy --workspace --all-targets cargo clippy --workspace --all-targets
cargo run -p loss-harness # FEC loss-resilience sweep (no network needed) 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 The C header regenerates from `crates/punktfunk-core/src/abi.rs` on every build (cbindgen via
`build.rs`) into `include/lumen_core.h`. `build.rs`) into `include/punktfunk_core.h`.
## Design invariants ## Design invariants
- **One core, linked everywhere.** Protocol/FEC/crypto/pacing live in `lumen-core` exactly - **One core, linked everywhere.** Protocol/FEC/crypto/pacing live in `punktfunk-core` exactly
once, exposed over a stable, versioned C ABI (`lumen_abi_version()`, `LumenConfig` once, exposed over a stable, versioned C ABI (`punktfunk_abi_version()`, `PunktfunkConfig`
carries its own `struct_size`). carries its own `struct_size`).
- **No async on the hot path.** The per-frame pipeline uses native threads only; - **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). `tokio`/`quinn` are gated behind the off-by-default `quic` feature (control plane only).
+6 -6
View File
@@ -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 ## Wiring
1. Build the core as a shared library per Android ABI: 1. Build the core as a shared library per Android ABI:
```sh ```sh
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android 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.) (Use `cargo-ndk` to handle the NDK toolchain/linker.)
2. JNI shim: small C/Rust glue mapping `lumen_*` to Kotlin `external fun`s, bundling 2. JNI shim: small C/Rust glue mapping `punktfunk_*` to Kotlin `external fun`s, bundling
`liblumen_core.so` into the APK's `jniLibs/`. `libpunktfunk_core.so` into the APK's `jniLibs/`.
3. Kotlin: client `LumenSession` → `lumen_client_poll_frame` on a decode thread → feed 3. Kotlin: client `PunktfunkSession` → `punktfunk_client_poll_frame` on a decode thread → feed
`MediaCodec` → render to a `SurfaceView` aligned to the display refresh. `MediaCodec` → render to a `SurfaceView` aligned to the display refresh.
## Status ## Status
+11 -11
View File
@@ -1,21 +1,21 @@
// swift-tools-version: 5.9 // swift-tools-version: 5.9
// LumenKit Swift wrapper around the lumen-core C ABI (lumen/1 client connector) plus the // PunktfunkKit Swift wrapper around the punktfunk-core C ABI (punktfunk/1 client connector) plus the
// SwiftUI/VideoToolbox presentation layer. Build LumenCore.xcframework first: // SwiftUI/VideoToolbox presentation layer. Build PunktfunkCore.xcframework first:
// bash ../../scripts/build-xcframework.sh (on a Mac; see README.md) // bash ../../scripts/build-xcframework.sh (on a Mac; see README.md)
import PackageDescription import PackageDescription
let package = Package( let package = Package(
name: "LumenKit", name: "PunktfunkKit",
platforms: [.macOS(.v14), .iOS(.v17)], platforms: [.macOS(.v14), .iOS(.v17)],
products: [ products: [
.library(name: "LumenKit", targets: ["LumenKit"]), .library(name: "PunktfunkKit", targets: ["PunktfunkKit"]),
.executable(name: "LumenClient", targets: ["LumenClient"]), .executable(name: "PunktfunkClient", targets: ["PunktfunkClient"]),
], ],
targets: [ targets: [
.binaryTarget(name: "LumenCore", path: "LumenCore.xcframework"), .binaryTarget(name: "PunktfunkCore", path: "PunktfunkCore.xcframework"),
.target( .target(
name: "LumenKit", name: "PunktfunkKit",
dependencies: ["LumenCore"], dependencies: ["PunktfunkCore"],
linkerSettings: [ linkerSettings: [
// Rust staticlib system deps. // Rust staticlib system deps.
.linkedFramework("Security"), .linkedFramework("Security"),
@@ -23,8 +23,8 @@ let package = Package(
.linkedLibrary("resolv"), .linkedLibrary("resolv"),
] ]
), ),
// Development app shell (swift run LumenClient): connect form stream + input. // Development app shell (swift run PunktfunkClient): connect form stream + input.
.executableTarget(name: "LumenClient", dependencies: ["LumenKit"]), .executableTarget(name: "PunktfunkClient", dependencies: ["PunktfunkKit"]),
.testTarget(name: "LumenKitTests", dependencies: ["LumenKit"]), .testTarget(name: "PunktfunkKitTests", dependencies: ["PunktfunkKit"]),
] ]
) )
+20 -20
View File
@@ -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, 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 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. (VideoToolbox), present (SwiftUI), input capture.
## Status — first light achieved (2026-06-10) ## Status — first light achieved (2026-06-10)
Validated live, Mac ↔ Linux box over the LAN: gamescope virtual output → NVENC HEVC → 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 `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 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 the session). Headless variant of the same proof: `RemoteFirstLightTests` decoded 60/60
received AUs spanning 983 ms of host capture clock. 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()`), full session: video AUs, **Opus audio** (`nextAudio()`), **rumble** (`nextRumble()`),
input incl. gamepads, and **cert pinning + TOFU** (`pinSHA256:`/`hostFingerprint`) — see input incl. gamepads, and **cert pinning + TOFU** (`pinSHA256:`/`hostFingerprint`) — see
`m3.rs::tests::c_abi_connection_roundtrip` (three sequential sessions: TOFU, pinned `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. reconnect at will during development.
What's here, all compiled and tested on macOS (Xcode 26.5 / Swift 6.3): What's here, all compiled and tested on macOS (Xcode 26.5 / Swift 6.3):
- **`LumenKit`** (library) - **`PunktfunkKit`** (library)
- `LumenConnection.swift` — wrapper over the C ABI. AUs/audio are copied into `Data` - `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 (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 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 `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 `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 motion isn't truncated away. Buttons use GameStream ids (1=left … 5=X2); scroll is
WHEEL_DELTA(120)-scaled. 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 (Audio playback and gamepad capture are not wired into the app yet — the connector
surface is there; see notes 56.) surface is there; see notes 56.)
- **Tests** (`swift test`): byte-level Annex-B units; a real-codec round trip - **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 ```sh
rustup target add aarch64-apple-darwin x86_64-apple-darwin 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 cd clients/apple
swift build && swift test # loopback/remote tests self-skip without a host 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 # (synthetic source — runs on macOS), streams
# byte-verified frames into the Swift client # byte-verified frames into the Swift client
# against the real host (Linux box, see CLAUDE.md "Running on this box") — m3-host is a # against the real host (Linux box, see CLAUDE.md "Running on this box") — m3-host is a
# persistent listener, reconnect at will: # persistent listener, reconnect at will:
# LUMEN_COMPOSITOR=gamescope LUMEN_GAMESCOPE_APP=vkcube LUMEN_ZEROCOPY=1 \ # PUNKTFUNK_COMPOSITOR=gamescope PUNKTFUNK_GAMESCOPE_APP=vkcube PUNKTFUNK_ZEROCOPY=1 \
# cargo run -rp lumen-host -- m3-host --source virtual --seconds 60 # cargo run -rp punktfunk-host -- m3-host --source virtual --seconds 60
LUMEN_REMOTE_HOST=<box-ip> swift test --filter RemoteFirstLightTests # headless PUNKTFUNK_REMOTE_HOST=<box-ip> swift test --filter RemoteFirstLightTests # headless
LUMEN_AUTOCONNECT=<box-ip> LUMEN_MODE=1280x720x60 swift run LumenClient # on glass PUNKTFUNK_AUTOCONNECT=<box-ip> PUNKTFUNK_MODE=1280x720x60 swift run PunktfunkClient # on glass
``` ```
## Notes for whoever picks this up next ## Notes for whoever picks this up next
1. **cbindgen import quirk** (the predicted "small compile fixes", now fixed): the 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 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* 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 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 slots, so the planes never alias); `send()` is enqueue-only and safe alongside all of
@@ -91,7 +91,7 @@ LUMEN_AUTOCONNECT=<box-ip> LUMEN_MODE=1280x720x60 swift run LumenClient # on gla
`AVAudioEngine` source node; conceal gaps (drop/dup) rather than blocking — the Rust `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 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 `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 6. **Gamepads**: `GCController``.gamepadButton(...)`/`.gamepadAxis(...)` events (wire
contract documented on the constructors; the host accumulates them into a virtual contract documented on the constructors; the host accumulates them into a virtual
Xbox 360 pad). Poll `nextRumble()` and feed `GCDeviceHaptics` for force feedback. Xbox 360 pad). Poll `nextRumble()` and feed `GCDeviceHaptics` for force feedback.
@@ -99,7 +99,7 @@ LUMEN_AUTOCONNECT=<box-ip> LUMEN_MODE=1280x720x60 swift run LumenClient # on gla
7. **Trust**: connect once with `pinSHA256: nil` (TOFU), persist `hostFingerprint` keyed 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 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 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. 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 — 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 on focus loss `InputCapture` auto-releases everything still held (keys + buttons) so
@@ -1,16 +1,16 @@
// Connect form live stream. Stage-1 UX: pick host + mode, see frames, type/aim. // Connect form live stream. Stage-1 UX: pick host + mode, see frames, type/aim.
import AppKit import AppKit
import LumenKit import PunktfunkKit
import SwiftUI import SwiftUI
struct ContentView: View { struct ContentView: View {
@StateObject private var model = SessionModel() @StateObject private var model = SessionModel()
@AppStorage("lumen.host") private var host = "192.168.1.70" @AppStorage("punktfunk.host") private var host = "192.168.1.70"
@AppStorage("lumen.port") private var port = 9777 @AppStorage("punktfunk.port") private var port = 9777
@AppStorage("lumen.width") private var width = 1920 @AppStorage("punktfunk.width") private var width = 1920
@AppStorage("lumen.height") private var height = 1080 @AppStorage("punktfunk.height") private var height = 1080
@AppStorage("lumen.hz") private var hz = 60 @AppStorage("punktfunk.hz") private var hz = 60
var body: some View { var body: some View {
Group { Group {
@@ -24,17 +24,17 @@ struct ContentView: View {
.onDisappear { model.disconnect() } // window closed mid-session (Cmd+N spawns more) .onDisappear { model.disconnect() } // window closed mid-session (Cmd+N spawns more)
} }
/// Development hook: LUMEN_AUTOCONNECT=host[:port] connects immediately at the saved /// Development hook: PUNKTFUNK_AUTOCONNECT=host[:port] connects immediately at the saved
/// (or LUMEN_MODE=WxHxHz) mode lets scripts drive first-light runs. (IPv4/hostname /// (or PUNKTFUNK_MODE=WxHxHz) mode lets scripts drive first-light runs. (IPv4/hostname
/// only; an IPv6 literal would need bracket parsing.) /// only; an IPv6 literal would need bracket parsing.)
private func autoConnectIfAsked() { 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 !target.isEmpty, model.connection == nil, !model.connecting
else { return } else { return }
let parts = target.split(separator: ":") let parts = target.split(separator: ":")
host = String(parts[0]) host = String(parts[0])
if parts.count == 2, let p = Int(parts[1]) { port = p } 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) } let dims = mode.split(separator: "x").compactMap { Int($0) }
if dims.count == 3 { if dims.count == 3 {
width = dims[0] width = dims[0]
@@ -48,7 +48,7 @@ struct ContentView: View {
hz: UInt32(clamping: hz)) hz: UInt32(clamping: hz))
} }
private func stream(_ conn: LumenConnection) -> some View { private func stream(_ conn: PunktfunkConnection) -> some View {
StreamView( StreamView(
connection: conn, connection: conn,
onFrame: { [meter = model.meter] au in meter.note(byteCount: au.data.count) }, onFrame: { [meter = model.meter] au in meter.note(byteCount: au.data.count) },
@@ -61,7 +61,7 @@ struct ContentView: View {
.background(Color.black) .background(Color.black)
} }
private func hud(_ conn: LumenConnection) -> some View { private func hud(_ conn: PunktfunkConnection) -> some View {
VStack(alignment: .trailing, spacing: 4) { VStack(alignment: .trailing, spacing: 4) {
Text("\(conn.width)×\(conn.height)@\(conn.refreshHz) \(model.fps) fps \(model.mbps, specifier: "%.1f") Mb/s") Text("\(conn.width)×\(conn.height)@\(conn.refreshHz) \(model.fps) fps \(model.mbps, specifier: "%.1f") Mb/s")
.font(.system(.caption, design: .monospaced)) .font(.system(.caption, design: .monospaced))
@@ -76,7 +76,7 @@ struct ContentView: View {
private var connectForm: some View { private var connectForm: some View {
VStack(spacing: 14) { VStack(spacing: 14) {
Text("lumen").font(.largeTitle.weight(.semibold)) Text("punktfunk").font(.largeTitle.weight(.semibold))
Form { Form {
TextField("Host", text: $host) TextField("Host", text: $host)
TextField("Port", value: $port, format: .number.grouping(.never)) TextField("Port", value: $port, format: .number.grouping(.never))
@@ -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. // Connect form StreamView (AVSampleBufferDisplayLayer HEVC) + InputCapture.
import AppKit import AppKit
import SwiftUI import SwiftUI
@main @main
struct LumenClientApp: App { struct PunktfunkClientApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
var body: some Scene { var body: some Scene {
WindowGroup("lumen") { WindowGroup("punktfunk") {
ContentView() ContentView()
} }
} }
@@ -2,7 +2,7 @@
// pump-thread main-actor stats relay. // pump-thread main-actor stats relay.
import Foundation import Foundation
import LumenKit import PunktfunkKit
import SwiftUI import SwiftUI
/// Pump-thread-side frame counters; a 1 Hz main-actor timer drains them into @Published /// 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 @MainActor
final class SessionModel: ObservableObject { final class SessionModel: ObservableObject {
@Published var connection: LumenConnection? @Published var connection: PunktfunkConnection?
@Published var connecting = false @Published var connecting = false
@Published var errorMessage: String? @Published var errorMessage: String?
@Published var fps = 0 @Published var fps = 0
@@ -51,8 +51,8 @@ final class SessionModel: ObservableObject {
connecting = true connecting = true
errorMessage = nil errorMessage = nil
Task.detached(priority: .userInitiated) { Task.detached(priority: .userInitiated) {
// LumenConnection.init blocks on the QUIC handshake keep it off the main actor. // PunktfunkConnection.init blocks on the QUIC handshake keep it off the main actor.
let result = Result { try LumenConnection( let result = Result { try PunktfunkConnection(
host: host, port: port, width: width, height: height, refreshHz: hz) } host: host, port: port, width: width, height: height, refreshHz: hz) }
await MainActor.run { [weak self] in await MainActor.run { [weak self] in
guard let self else { return } guard let self else { return }
@@ -64,7 +64,7 @@ final class SessionModel: ObservableObject {
self.startStatsTimer() self.startStatsTimer()
case .failure: case .failure:
self.errorMessage = "Connection failed — is the host running? " + 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." errorMessage = "Session ended by host."
} }
private func startInput(_ conn: LumenConnection) { private func startInput(_ conn: PunktfunkConnection) {
let capture = InputCapture(connection: conn) let capture = InputCapture(connection: conn)
capture.start() capture.start()
inputCapture = capture inputCapture = capture
@@ -1,6 +1,6 @@
// Annex-B HEVC CoreMedia plumbing. // 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 // (deliberately the client needs no out-of-band extradata). VideoToolbox wants the AVCC
// flavor instead: a CMVideoFormatDescription built from the parameter sets, and sample // 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. // buffers whose NALs are 4-byte-length-prefixed. This file converts between the two.
@@ -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 // 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 // injector expects for relative motion. GCKeyboard gives HID keycodes which we map to the
@@ -22,12 +22,12 @@
import AppKit import AppKit
import Foundation import Foundation
import GameController import GameController
import LumenCore import PunktfunkCore
public final class InputCapture { public final class InputCapture {
private static weak var activeCapture: InputCapture? private static weak var activeCapture: InputCapture?
private let connection: LumenConnection private let connection: PunktfunkConnection
private var observers: [NSObjectProtocol] = [] private var observers: [NSObjectProtocol] = []
private var mice: [GCMouse] = [] private var mice: [GCMouse] = []
private var keyboards: [GCKeyboard] = [] private var keyboards: [GCKeyboard] = []
@@ -40,7 +40,7 @@ public final class InputCapture {
private var pressedVKs: Set<UInt32> = [] private var pressedVKs: Set<UInt32> = []
private var pressedButtons: Set<UInt32> = [] private var pressedButtons: Set<UInt32> = []
public init(connection: LumenConnection) { public init(connection: PunktfunkConnection) {
self.connection = connection 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 /// 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] = { static let hidToVK: [Int: UInt32] = {
var m: [Int: UInt32] = [:] var m: [Int: UInt32] = [:]
// az: HID 0x04..0x1D VK 'A'..'Z'. // az: HID 0x04..0x1D VK 'A'..'Z'.
@@ -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) // 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; // 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 // 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. // close, the pull methods throw `.closed` and the threads unwind on their own.
import Foundation import Foundation
import LumenCore import PunktfunkCore
// cbindgen's C17-compatible header spells the typedefs as plain integers // 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. // 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 statusOK: Int32 = PUNKTFUNK_STATUS_OK.rawValue
private let statusNoFrame: Int32 = LUMEN_STATUS_NO_FRAME.rawValue private let statusNoFrame: Int32 = PUNKTFUNK_STATUS_NO_FRAME.rawValue
private let statusClosed: Int32 = LUMEN_STATUS_CLOSED.rawValue private let statusClosed: Int32 = PUNKTFUNK_STATUS_CLOSED.rawValue
/// One reassembled, FEC-recovered, decrypted access unit (Annex-B HEVC from the host). /// One reassembled, FEC-recovered, decrypted access unit (Annex-B HEVC from the host).
public struct AccessUnit: Sendable { public struct AccessUnit: Sendable {
@@ -43,7 +43,7 @@ public struct AudioPacket: Sendable {
public let seq: UInt32 public let seq: UInt32
} }
public enum LumenClientError: Error { public enum PunktfunkClientError: Error {
/// Connect failed wrong host/port, timeout, or a certificate-pin mismatch. /// Connect failed wrong host/port, timeout, or a certificate-pin mismatch.
case connectFailed case connectFailed
/// `pinSHA256` was non-nil but not exactly 32 bytes. Failing closed: connecting /// `pinSHA256` was non-nil but not exactly 32 bytes. Failing closed: connecting
@@ -53,7 +53,7 @@ public enum LumenClientError: Error {
case status(Int32) case status(Int32)
} }
public final class LumenConnection { public final class PunktfunkConnection {
private var handle: OpaquePointer? private var handle: OpaquePointer?
/// Set by close() before it contends for the plane locks: the pullers see it at their /// 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 /// 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, pinSHA256: Data? = nil,
timeoutMs: UInt32 = 10_000 timeoutMs: UInt32 = 10_000
) throws { ) 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) var observed = [UInt8](repeating: 0, count: 32)
handle = host.withCString { cs in handle = host.withCString { cs in
if let pin = pinSHA256 { if let pin = pinSHA256 {
return pin.withUnsafeBytes { p in return pin.withUnsafeBytes { p in
lumen_connect( punktfunk_connect(
cs, port, width, height, refreshHz, cs, port, width, height, refreshHz,
p.bindMemory(to: UInt8.self).baseAddress, &observed, timeoutMs) 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) hostFingerprint = Data(observed)
var w: UInt32 = 0, h: UInt32 = 0, hz: UInt32 = 0 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.width = w
self.height = h self.height = h
self.refreshHz = hz self.refreshHz = hz
@@ -114,10 +114,10 @@ public final class LumenConnection {
public func nextAU(timeoutMs: UInt32 = 100) throws -> AccessUnit? { public func nextAU(timeoutMs: UInt32 = 100) throws -> AccessUnit? {
pumpLock.lock() pumpLock.lock()
defer { pumpLock.unlock() } defer { pumpLock.unlock() }
guard let h = liveHandle() else { throw LumenClientError.closed } guard let h = liveHandle() else { throw PunktfunkClientError.closed }
var frame = LumenFrame() var frame = PunktfunkFrame()
let rc = lumen_connection_next_au(h, &frame, timeoutMs) let rc = punktfunk_connection_next_au(h, &frame, timeoutMs)
switch rc { switch rc {
case statusOK: case statusOK:
guard let base = frame.data, frame.len > 0 else { return nil } guard let base = frame.data, frame.len > 0 else { return nil }
@@ -128,9 +128,9 @@ public final class LumenConnection {
case statusNoFrame: case statusNoFrame:
return nil return nil
case statusClosed: case statusClosed:
throw LumenClientError.closed throw PunktfunkClientError.closed
default: 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? { public func nextAudio(timeoutMs: UInt32 = 100) throws -> AudioPacket? {
audioLock.lock() audioLock.lock()
defer { audioLock.unlock() } defer { audioLock.unlock() }
guard let h = liveHandle() else { throw LumenClientError.closed } guard let h = liveHandle() else { throw PunktfunkClientError.closed }
var pkt = LumenAudioPacket() var pkt = PunktfunkAudioPacket()
let rc = lumen_connection_next_audio(h, &pkt, timeoutMs) let rc = punktfunk_connection_next_audio(h, &pkt, timeoutMs)
switch rc { switch rc {
case statusOK: case statusOK:
guard let base = pkt.data, pkt.len > 0 else { return nil } guard let base = pkt.data, pkt.len > 0 else { return nil }
@@ -152,9 +152,9 @@ public final class LumenConnection {
case statusNoFrame: case statusNoFrame:
return nil return nil
case statusClosed: case statusClosed:
throw LumenClientError.closed throw PunktfunkClientError.closed
default: 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)? { public func nextRumble(timeoutMs: UInt32 = 0) throws -> (pad: UInt16, low: UInt16, high: UInt16)? {
audioLock.lock() audioLock.lock()
defer { audioLock.unlock() } 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 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 { switch rc {
case statusOK: case statusOK:
return (pad, low, high) return (pad, low, high)
case statusNoFrame: case statusNoFrame:
return nil return nil
case statusClosed: case statusClosed:
throw LumenClientError.closed throw PunktfunkClientError.closed
default: default:
throw LumenClientError.status(rc) throw PunktfunkClientError.status(rc)
} }
} }
/// Send one input event (delivered to the host as a QUIC datagram). Thread-safe; /// Send one input event (delivered to the host as a QUIC datagram). Thread-safe;
/// silently dropped after close. /// silently dropped after close.
public func send(_ event: LumenInputEvent) { public func send(_ event: PunktfunkInputEvent) {
var ev = event var ev = event
abiLock.lock() abiLock.lock()
defer { abiLock.unlock() } defer { abiLock.unlock() }
guard let h = handle, !closeRequested else { return } 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 /// Close the connection and free the handle. Safe from any thread, idempotent; waits
@@ -205,7 +205,7 @@ public final class LumenConnection {
audioLock.unlock() audioLock.unlock()
pumpLock.unlock() pumpLock.unlock()
if let h { 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 // Convenience constructors for the wire input events (field semantics match
// lumen_core::input::InputEvent; see lumen_core.h). // punktfunk_core::input::InputEvent; see punktfunk_core.h).
public extension LumenInputEvent { public extension PunktfunkInputEvent {
private static func make( private static func make(
_ kind: UInt32, code: UInt32, x: Int32, y: Int32, flags: UInt32 = 0 _ kind: UInt32, code: UInt32, x: Int32, y: Int32, flags: UInt32 = 0
) -> LumenInputEvent { ) -> PunktfunkInputEvent {
LumenInputEvent(kind: UInt8(kind), _pad: (0, 0, 0), code: code, x: x, y: y, flags: flags) PunktfunkInputEvent(kind: UInt8(kind), _pad: (0, 0, 0), code: code, x: x, y: y, flags: flags)
} }
static func mouseMove(dx: Int32, dy: Int32) -> LumenInputEvent { static func mouseMove(dx: Int32, dy: Int32) -> PunktfunkInputEvent {
make(LUMEN_INPUT_KIND_MOUSE_MOVE.rawValue, code: 0, x: dx, y: dy) 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_*). /// 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( 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) code: button, x: 0, y: 0)
} }
/// `vk` is a Windows virtual-key code (the host's vk_to_evdev table consumes these). /// `vk` is a Windows virtual-key code (the host's vk_to_evdev table consumes these).
static func key(_ vk: UInt32, down: Bool) -> LumenInputEvent { static func key(_ vk: UInt32, down: Bool) -> PunktfunkInputEvent {
make((down ? LUMEN_INPUT_KIND_KEY_DOWN : LUMEN_INPUT_KIND_KEY_UP).rawValue, code: vk, x: 0, y: 0) 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 /// WHEEL_DELTA(120)-scaled; positive = up (vertical) / right (horizontal) the
/// convention Moonlight/SDL use; the host maps onto the ei/wl axes. /// convention Moonlight/SDL use; the host maps onto the ei/wl axes.
static func scroll(_ delta: Int32, horizontal: Bool = false) -> LumenInputEvent { static func scroll(_ delta: Int32, horizontal: Bool = false) -> PunktfunkInputEvent {
make(LUMEN_INPUT_KIND_MOUSE_SCROLL.rawValue, code: horizontal ? 1 : 0, x: delta, y: 0) 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. // `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, /// `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). /// 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( make(
LUMEN_INPUT_KIND_GAMEPAD_BUTTON.rawValue, PUNKTFUNK_INPUT_KIND_GAMEPAD_BUTTON.rawValue,
code: button, x: down ? 1 : 0, y: 0, flags: pad) 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 /// 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). /// `GCControllerDirectionPad.yAxis` already matches, no flip), 4=LT 5=RT (0...255).
static func gamepadAxis(_ axis: UInt32, value: Int32, pad: UInt32 = 0) -> LumenInputEvent { static func gamepadAxis(_ axis: UInt32, value: Int32, pad: UInt32 = 0) -> PunktfunkInputEvent {
make(LUMEN_INPUT_KIND_GAMEPAD_AXIS.rawValue, code: axis, x: value, y: 0, flags: pad) make(PUNKTFUNK_INPUT_KIND_GAMEPAD_AXIS.rawValue, code: axis, x: value, y: 0, flags: pad)
} }
} }
@@ -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 // Stage-1 presenter (see README): the layer accepts *compressed* HEVC sample buffers and
// does hardware decode + display itself fastest path to pixels, IOSurface-backed // does hardware decode + display itself fastest path to pixels, IOSurface-backed
@@ -13,13 +13,13 @@ import AVFoundation
import SwiftUI import SwiftUI
public struct StreamView: NSViewRepresentable { public struct StreamView: NSViewRepresentable {
private let connection: LumenConnection private let connection: PunktfunkConnection
private let onFrame: (@Sendable (AccessUnit) -> Void)? private let onFrame: (@Sendable (AccessUnit) -> Void)?
private let onSessionEnd: (@Sendable () -> Void)? private let onSessionEnd: (@Sendable () -> Void)?
/// `onFrame`/`onSessionEnd` fire on the pump thread hop to the main actor for UI. /// `onFrame`/`onSessionEnd` fire on the pump thread hop to the main actor for UI.
public init( public init(
connection: LumenConnection, connection: PunktfunkConnection,
onFrame: (@Sendable (AccessUnit) -> Void)? = nil, onFrame: (@Sendable (AccessUnit) -> Void)? = nil,
onSessionEnd: (@Sendable () -> Void)? = nil onSessionEnd: (@Sendable () -> Void)? = nil
) { ) {
@@ -67,7 +67,7 @@ public final class StreamLayerView: NSView {
private let displayLayer = AVSampleBufferDisplayLayer() private let displayLayer = AVSampleBufferDisplayLayer()
private var token: PumpToken? private var token: PumpToken?
public private(set) var connection: LumenConnection? public private(set) var connection: PunktfunkConnection?
public override init(frame: NSRect) { public override init(frame: NSRect) {
super.init(frame: frame) 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 /// 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). /// format description; non-IDR AUs before it are dropped (the host opens with an IDR).
public func start( public func start(
connection: LumenConnection, connection: PunktfunkConnection,
onFrame: (@Sendable (AccessUnit) -> Void)? = nil, onFrame: (@Sendable (AccessUnit) -> Void)? = nil,
onSessionEnd: (@Sendable () -> Void)? = nil onSessionEnd: (@Sendable () -> Void)? = nil
) { ) {
@@ -104,7 +104,7 @@ public final class StreamLayerView: NSView {
if layer.status == .failed { if layer.status == .failed {
// Decode wedged: flush and re-gate on the next in-band parameter // Decode wedged: flush and re-gate on the next in-band parameter
// sets resuming with a delta frame can't recover. (A // 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 // host's infinite GOP this may otherwise stay black until the
// next recovery keyframe.) // next recovery keyframe.)
layer.flush() layer.flush()
@@ -123,13 +123,13 @@ public final class StreamLayerView: NSView {
} }
} }
} }
thread.name = "lumen-pump" thread.name = "punktfunk-pump"
thread.qualityOfService = .userInteractive thread.qualityOfService = .userInteractive
thread.start() thread.start()
} }
/// Stop pumping ( one poll timeout). Does not close the connection that stays with /// 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() { public func stop() {
token?.cancel() token?.cancel()
token = nil token = nil
@@ -2,7 +2,7 @@
// VideoToolboxRoundTripTests covers the real-bitstream path). // VideoToolboxRoundTripTests covers the real-bitstream path).
import XCTest import XCTest
@testable import LumenKit @testable import PunktfunkKit
final class AnnexBTests: XCTestCase { final class AnnexBTests: XCTestCase {
/// NAL with the given HEVC type in bits 1..6 of the first header byte. /// NAL with the given HEVC type in bits 1..6 of the first header byte.
@@ -1,20 +1,20 @@
// Integration: the Swift wrapper against a real lumen/1 host over QUIC + UDP on loopback // Integration: the Swift wrapper against a real punktfunk/1 host over QUIC + UDP on loopback
// the Swift twin of lumen-host's m3.rs::c_abi_connection_roundtrip, this time through the // 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 // 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 import XCTest
@testable import LumenKit @testable import PunktfunkKit
final class LoopbackIntegrationTests: XCTestCase { final class LoopbackIntegrationTests: XCTestCase {
func testSyntheticStreamRoundTrip() throws { 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) let port = UInt16(portStr)
else { else {
throw XCTSkip("needs a running m3-host — use clients/apple/test-loopback.sh") 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) host: "127.0.0.1", port: port, width: 1280, height: 720, refreshHz: 60)
XCTAssertEqual(conn.width, 1280) XCTAssertEqual(conn.width, 1280)
XCTAssertEqual(conn.height, 720) XCTAssertEqual(conn.height, 720)
@@ -49,7 +49,7 @@ final class LoopbackIntegrationTests: XCTestCase {
conn.close() conn.close()
XCTAssertThrowsError(try conn.nextAU(timeoutMs: 10)) { error in 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)") return XCTFail("expected .closed, got \(error)")
} }
} }
@@ -58,7 +58,7 @@ final class LoopbackIntegrationTests: XCTestCase {
func testConnectFailureThrows() { func testConnectFailureThrows() {
// Nothing listens on this port; connect must fail within its timeout, not hang. // Nothing listens on this port; connect must fail within its timeout, not hang.
XCTAssertThrowsError( XCTAssertThrowsError(
try LumenConnection( try PunktfunkConnection(
host: "127.0.0.1", port: 9, width: 640, height: 480, refreshHz: 30, host: "127.0.0.1", port: 9, width: 640, height: 480, refreshHz: 30,
timeoutMs: 2000)) timeoutMs: 2000))
} }
@@ -4,25 +4,25 @@
// putting the layer on glass. // putting the layer on glass.
// //
// Run (host side, on the Linux box): // Run (host side, on the Linux box):
// LUMEN_COMPOSITOR=gamescope LUMEN_GAMESCOPE_APP=vkcube LUMEN_ZEROCOPY=1 \ // PUNKTFUNK_COMPOSITOR=gamescope PUNKTFUNK_GAMESCOPE_APP=vkcube PUNKTFUNK_ZEROCOPY=1 \
// lumen-host m3-host --source virtual --seconds 120 // punktfunk-host m3-host --source virtual --seconds 120
// Then here: // 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 CoreMedia
import VideoToolbox import VideoToolbox
import XCTest import XCTest
@testable import LumenKit @testable import PunktfunkKit
final class RemoteFirstLightTests: XCTestCase { final class RemoteFirstLightTests: XCTestCase {
func testRemoteStreamDecodesToPixels() throws { func testRemoteStreamDecodesToPixels() throws {
guard let host = ProcessInfo.processInfo.environment["LUMEN_REMOTE_HOST"] else { guard let host = ProcessInfo.processInfo.environment["PUNKTFUNK_REMOTE_HOST"] else {
throw XCTSkip("set LUMEN_REMOTE_HOST (and start m3-host --source virtual there)") throw XCTSkip("set PUNKTFUNK_REMOTE_HOST (and start m3-host --source virtual there)")
} }
let width: UInt32 = 1280 let width: UInt32 = 1280
let height: UInt32 = 720 let height: UInt32 = 720
let conn = try LumenConnection( let conn = try PunktfunkConnection(
host: host, width: width, height: height, refreshHz: 60) host: host, width: width, height: height, refreshHz: 60)
defer { conn.close() } defer { conn.close() }
XCTAssertEqual(conn.width, width) XCTAssertEqual(conn.width, width)
@@ -1,13 +1,13 @@
// Real-bitstream proof of the decode-prep path: VTCompressionSession encodes HEVC, we // 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 // 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. // VTDecompressionSession. Pixels out = the whole client decode path is sound.
import AVFoundation import AVFoundation
import CoreMedia import CoreMedia
import VideoToolbox import VideoToolbox
import XCTest import XCTest
@testable import LumenKit @testable import PunktfunkKit
final class VideoToolboxRoundTripTests: XCTestCase { final class VideoToolboxRoundTripTests: XCTestCase {
private let width = 320 private let width = 320
+5 -5
View File
@@ -1,17 +1,17 @@
#!/usr/bin/env bash #!/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. # 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. # The m3 host serves exactly one session and exits; the trap is just for failure paths.
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")/../.." cd "$(dirname "$0")/../.."
PORT="${LUMEN_LOOPBACK_PORT:-19778}" PORT="${PUNKTFUNK_LOOPBACK_PORT:-19778}"
cargo build --release -p lumen-host cargo build --release -p punktfunk-host
target/release/lumen-host m3-host --port "$PORT" --source synthetic --frames 300 & target/release/punktfunk-host m3-host --port "$PORT" --source synthetic --frames 300 &
HOST_PID=$! HOST_PID=$!
trap 'kill "$HOST_PID" 2>/dev/null || true' EXIT trap 'kill "$HOST_PID" 2>/dev/null || true' EXIT
sleep 1 sleep 1
cd clients/apple cd clients/apple
LUMEN_LOOPBACK_PORT="$PORT" swift test --filter LoopbackIntegrationTests PUNKTFUNK_LOOPBACK_PORT="$PORT" swift test --filter LoopbackIntegrationTests
-56
View File
@@ -1,56 +0,0 @@
language = "C"
pragma_once = true
include_guard = "LUMEN_CORE_H"
autogen_warning = "/* Generated by cbindgen from lumen-core. Do not edit by hand. */"
header = "/* lumen-core C ABI — see crates/lumen-core/src/abi.rs */"
style = "type"
cpp_compat = true
tab_width = 4
documentation = true
documentation_style = "c99"
[parse]
parse_deps = false
[export.rename]
"InputEvent" = "LumenInputEvent"
"InputKind" = "LumenInputKind"
# Gamepad wire constants: bare BTN_* names collide with <linux/input-event-codes.h> (at
# DIFFERENT values — last definition silently wins); prefix everything we export.
"BTN_DPAD_UP" = "LUMEN_BTN_DPAD_UP"
"BTN_DPAD_DOWN" = "LUMEN_BTN_DPAD_DOWN"
"BTN_DPAD_LEFT" = "LUMEN_BTN_DPAD_LEFT"
"BTN_DPAD_RIGHT" = "LUMEN_BTN_DPAD_RIGHT"
"BTN_START" = "LUMEN_BTN_START"
"BTN_BACK" = "LUMEN_BTN_BACK"
"BTN_LS_CLICK" = "LUMEN_BTN_LS_CLICK"
"BTN_RS_CLICK" = "LUMEN_BTN_RS_CLICK"
"BTN_LB" = "LUMEN_BTN_LB"
"BTN_RB" = "LUMEN_BTN_RB"
"BTN_GUIDE" = "LUMEN_BTN_GUIDE"
"BTN_A" = "LUMEN_BTN_A"
"BTN_B" = "LUMEN_BTN_B"
"BTN_X" = "LUMEN_BTN_X"
"BTN_Y" = "LUMEN_BTN_Y"
"AXIS_LS_X" = "LUMEN_AXIS_LS_X"
"AXIS_LS_Y" = "LUMEN_AXIS_LS_Y"
"AXIS_RS_X" = "LUMEN_AXIS_RS_X"
"AXIS_RS_Y" = "LUMEN_AXIS_RS_Y"
"AXIS_LT" = "LUMEN_AXIS_LT"
"AXIS_RT" = "LUMEN_AXIS_RT"
"AUDIO_MAGIC" = "LUMEN_AUDIO_MAGIC"
"RUMBLE_MAGIC" = "LUMEN_RUMBLE_MAGIC"
# QualifiedScreamingSnakeCase already qualifies each variant with the enum name
# (LumenStatus::Ok -> LUMEN_STATUS_OK); do NOT also set prefix_with_name or it doubles.
[enum]
rename_variants = "QualifiedScreamingSnakeCase"
[fn]
sort_by = "None"
[struct]
derive_eq = false
[defines]
"feature = quic" = "LUMEN_FEATURE_QUIC"
@@ -1,6 +1,6 @@
[package] [package]
name = "lumen-client-rs" name = "punktfunk-client-rs"
description = "lumen reference client (M4): VAAPI decode + wgpu/Vulkan present" description = "punktfunk reference client (M4): VAAPI decode + wgpu/Vulkan present"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@@ -9,7 +9,7 @@ authors.workspace = true
repository.workspace = true repository.workspace = true
[dependencies] [dependencies]
lumen-core = { path = "../lumen-core", features = ["quic"] } punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
quinn = "0.11" quinn = "0.11"
tokio = { version = "1", features = ["rt-multi-thread", "net", "time", "macros"] } tokio = { version = "1", features = ["rt-multi-thread", "net", "time", "macros"] }
anyhow = "1" anyhow = "1"
@@ -1,4 +1,4 @@
//! `lumen-client-rs` — the reference client for `lumen/1` (M3): QUIC control plane, UDP data //! `punktfunk-client-rs` — the reference client for `punktfunk/1` (M3): QUIC control plane, UDP data
//! plane, input over QUIC datagrams. Two modes, decided by the host's Welcome: //! plane, input over QUIC datagrams. Two modes, decided by the host's Welcome:
//! //!
//! * **verification** (`frames > 0`, synthetic host): byte-checks deterministic test frames; //! * **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 //! Host→client datagrams (Opus audio, rumble) are counted and reported with the stream
//! stats — decode/playback is the platform clients' job. //! 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.) //! [--pin HEX]` (M4 adds VAAPI decode + wgpu present on this same skeleton.)
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use lumen_core::config::Role; use punktfunk_core::config::Role;
use lumen_core::input::{InputEvent, InputKind}; use punktfunk_core::input::{InputEvent, InputKind};
use lumen_core::quic::{endpoint, io, Hello, Start, Welcome}; use punktfunk_core::quic::{endpoint, io, Hello, Start, Welcome};
use lumen_core::transport::UdpTransport; use punktfunk_core::transport::UdpTransport;
use lumen_core::{LumenError, Mode, Session}; use punktfunk_core::{Mode, PunktfunkError, Session};
use std::io::Write; use std::io::Write;
struct Args { struct Args {
@@ -126,25 +126,25 @@ async fn session(args: Args) -> Result<()> {
let (ep, observed) = endpoint::client_pinned(args.pin); let (ep, observed) = endpoint::client_pinned(args.pin);
let ep = ep.map_err(|e| anyhow!("QUIC client endpoint: {e}"))?; let ep = ep.map_err(|e| anyhow!("QUIC client endpoint: {e}"))?;
let conn = ep let conn = ep
.connect(remote, "lumen") .connect(remote, "punktfunk")
.context("connect")? .context("connect")?
.await .await
.context("QUIC handshake (a pin mismatch fails here)")?; .context("QUIC handshake (a pin mismatch fails here)")?;
match (args.pin, *observed.lock().unwrap()) { 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!( (None, Some(fp)) => tracing::info!(
%remote, %remote,
fingerprint = %hex(&fp), 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")?; let (mut send, mut recv) = conn.open_bi().await.context("open control stream")?;
io::write_msg( io::write_msg(
&mut send, &mut send,
&Hello { &Hello {
abi_version: lumen_core::ABI_VERSION, abi_version: punktfunk_core::ABI_VERSION,
mode: args.mode, mode: args.mode,
} }
.encode(), .encode(),
@@ -210,7 +210,7 @@ async fn session(args: Args) -> Result<()> {
} }
// Gamepad plane: tap A + sweep the left stick on pad 0 (the host // Gamepad plane: tap A + sweep the left stick on pad 0 (the host
// accumulates these into its virtual xpad; needs /dev/uinput access). // 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 = [ let pad_events = [
(InputKind::GamepadButton, BTN_A, 1), (InputKind::GamepadButton, BTN_A, 1),
(InputKind::GamepadButton, BTN_A, 0), (InputKind::GamepadButton, BTN_A, 0),
@@ -260,10 +260,10 @@ async fn session(args: Args) -> Result<()> {
tokio::spawn(async move { tokio::spawn(async move {
use std::sync::atomic::Ordering::Relaxed; use std::sync::atomic::Ordering::Relaxed;
while let Ok(d) = conn2.read_datagram().await { 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); a.fetch_add(1, Relaxed);
ab.fetch_add(opus.len() as u64, 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); 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)); std::thread::sleep(std::time::Duration::from_micros(300));
} }
Err(e) => return Err(anyhow!("poll_frame: {e:?}")), 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_p95_us = pct(0.95),
lat_p99_us = pct(0.99), lat_p99_us = pct(0.99),
lat_max_us = latencies_us.last().copied().unwrap_or(0), 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 { if expected > 0 {
anyhow::ensure!(mismatched == 0, "{mismatched} corrupted frames"); anyhow::ensure!(mismatched == 0, "{mismatched} corrupted frames");
@@ -394,7 +394,7 @@ async fn session(args: Args) -> Result<()> {
result result
} }
/// The host's deterministic test frame (mirror of `lumen-host::m3::test_frame`). /// The host's deterministic test frame (mirror of `punktfunk-host::m3::test_frame`).
fn test_frame(idx: u32, len: usize) -> Vec<u8> { fn test_frame(idx: u32, len: usize) -> Vec<u8> {
let mut d = vec![0u8; len]; let mut d = vec![0u8; len];
if len >= 4 { if len >= 4 {
@@ -1,6 +1,6 @@
[package] [package]
name = "lumen-core" name = "punktfunk-core"
description = "lumen shared protocol/transport/FEC core, exposed over a stable C ABI" description = "punktfunk shared protocol/transport/FEC core, exposed over a stable C ABI"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@@ -9,10 +9,10 @@ authors.workspace = true
repository.workspace = true repository.workspace = true
[lib] [lib]
name = "lumen_core" name = "punktfunk_core"
# `lib` — so lumen-host / lumen-client-rs / tools link it as a normal Rust crate. # `lib` — so punktfunk-host / punktfunk-client-rs / tools link it as a normal Rust crate.
# `staticlib` — `liblumen_core.a` for the C test harness and static embedding. # `staticlib` — `libpunktfunk_core.a` for the C test harness and static embedding.
# `cdylib` — `liblumen_core.{so,dylib}` for Swift/Kotlin clients via the C ABI. # `cdylib` — `libpunktfunk_core.{so,dylib}` for Swift/Kotlin clients via the C ABI.
crate-type = ["lib", "cdylib", "staticlib"] crate-type = ["lib", "cdylib", "staticlib"]
[features] [features]
@@ -1,4 +1,4 @@
//! Generate the C header (`include/lumen_core.h`) from the `extern "C"` surface. //! Generate the C header (`include/punktfunk_core.h`) from the `extern "C"` surface.
//! //!
//! cbindgen failure is a warning, not a hard error, so the crate still builds in minimal //! 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. //! 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"); println!("cargo:rerun-if-changed=cbindgen.toml");
let crate_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"); 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) let out = PathBuf::from(&crate_dir)
.join("..") .join("..")
.join("..") .join("..")
.join("include") .join("include")
.join("lumen_core.h"); .join("punktfunk_core.h");
match cbindgen::generate(&crate_dir) { match cbindgen::generate(&crate_dir) {
Ok(bindings) => { Ok(bindings) => {
bindings.write_to_file(&out); bindings.write_to_file(&out);
println!("cargo:warning=lumen-core: wrote {}", out.display()); println!("cargo:warning=punktfunk-core: wrote {}", out.display());
} }
Err(e) => { Err(e) => {
println!("cargo:warning=lumen-core: cbindgen failed ({e}); header not regenerated"); println!("cargo:warning=punktfunk-core: cbindgen failed ({e}); header not regenerated");
} }
} }
} }
+56
View File
@@ -0,0 +1,56 @@
language = "C"
pragma_once = true
include_guard = "PUNKTFUNK_CORE_H"
autogen_warning = "/* Generated by cbindgen from punktfunk-core. Do not edit by hand. */"
header = "/* punktfunk-core C ABI — see crates/punktfunk-core/src/abi.rs */"
style = "type"
cpp_compat = true
tab_width = 4
documentation = true
documentation_style = "c99"
[parse]
parse_deps = false
[export.rename]
"InputEvent" = "PunktfunkInputEvent"
"InputKind" = "PunktfunkInputKind"
# Gamepad wire constants: bare BTN_* names collide with <linux/input-event-codes.h> (at
# DIFFERENT values — last definition silently wins); prefix everything we export.
"BTN_DPAD_UP" = "PUNKTFUNK_BTN_DPAD_UP"
"BTN_DPAD_DOWN" = "PUNKTFUNK_BTN_DPAD_DOWN"
"BTN_DPAD_LEFT" = "PUNKTFUNK_BTN_DPAD_LEFT"
"BTN_DPAD_RIGHT" = "PUNKTFUNK_BTN_DPAD_RIGHT"
"BTN_START" = "PUNKTFUNK_BTN_START"
"BTN_BACK" = "PUNKTFUNK_BTN_BACK"
"BTN_LS_CLICK" = "PUNKTFUNK_BTN_LS_CLICK"
"BTN_RS_CLICK" = "PUNKTFUNK_BTN_RS_CLICK"
"BTN_LB" = "PUNKTFUNK_BTN_LB"
"BTN_RB" = "PUNKTFUNK_BTN_RB"
"BTN_GUIDE" = "PUNKTFUNK_BTN_GUIDE"
"BTN_A" = "PUNKTFUNK_BTN_A"
"BTN_B" = "PUNKTFUNK_BTN_B"
"BTN_X" = "PUNKTFUNK_BTN_X"
"BTN_Y" = "PUNKTFUNK_BTN_Y"
"AXIS_LS_X" = "PUNKTFUNK_AXIS_LS_X"
"AXIS_LS_Y" = "PUNKTFUNK_AXIS_LS_Y"
"AXIS_RS_X" = "PUNKTFUNK_AXIS_RS_X"
"AXIS_RS_Y" = "PUNKTFUNK_AXIS_RS_Y"
"AXIS_LT" = "PUNKTFUNK_AXIS_LT"
"AXIS_RT" = "PUNKTFUNK_AXIS_RT"
"AUDIO_MAGIC" = "PUNKTFUNK_AUDIO_MAGIC"
"RUMBLE_MAGIC" = "PUNKTFUNK_RUMBLE_MAGIC"
# QualifiedScreamingSnakeCase already qualifies each variant with the enum name
# (PunktfunkStatus::Ok -> PUNKTFUNK_STATUS_OK); do NOT also set prefix_with_name or it doubles.
[enum]
rename_variants = "QualifiedScreamingSnakeCase"
[fn]
sort_by = "None"
[struct]
derive_eq = false
[defines]
"feature = quic" = "PUNKTFUNK_FEATURE_QUIC"
@@ -1,17 +1,17 @@
//! The stable `extern "C"` surface. `cbindgen` turns this module into //! 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) //! ## 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. //! - All cross-boundary structs are `#[repr(C)]`; buffers are pointer + length.
//! - Explicit ownership: every handle from `*_new` / `*_pair` must be passed to //! - 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. //! `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`. //! - Panics never cross the boundary: every entry point is wrapped in `catch_unwind`.
use crate::config::{Config, FecConfig, FecScheme, ProtocolPhase, Role}; use crate::config::{Config, FecConfig, FecScheme, ProtocolPhase, Role};
use crate::error::LumenStatus; use crate::error::PunktfunkStatus;
use crate::input::InputEvent; use crate::input::InputEvent;
use crate::session::Session; use crate::session::Session;
use crate::stats::Stats; use crate::stats::Stats;
@@ -22,23 +22,23 @@ use std::panic::AssertUnwindSafe;
use std::ptr; use std::ptr;
/// Opaque session handle. Pointer-only from C. /// Opaque session handle. Pointer-only from C.
pub struct LumenSession { pub struct PunktfunkSession {
inner: Session, 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. /// until the next poll or free.
last_frame: Option<crate::session::Frame>, last_frame: Option<crate::session::Frame>,
input_cb: Option<(LumenInputCb, *mut c_void)>, input_cb: Option<(PunktfunkInputCb, *mut c_void)>,
} }
/// Forward-compatible session configuration. The caller MUST set `struct_size` to /// 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)] #[repr(C)]
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct LumenConfig { pub struct PunktfunkConfig {
pub struct_size: u32, pub struct_size: u32,
/// 0 = host, 1 = client. /// 0 = host, 1 = client.
pub role: u32, pub role: u32,
/// 1 = P1 (GameStream-compatible), 2 = P2 (`lumen/1`). /// 1 = P1 (GameStream-compatible), 2 = P2 (`punktfunk/1`).
pub phase: u32, pub phase: u32,
/// 0 = GF(2⁸), 1 = GF(2¹⁶). /// 0 = GF(2⁸), 1 = GF(2¹⁶).
pub fec_scheme: u32, pub fec_scheme: u32,
@@ -55,27 +55,28 @@ pub struct LumenConfig {
pub max_frame_bytes: u64, pub max_frame_bytes: u64,
} }
impl LumenConfig { impl PunktfunkConfig {
fn to_config(self) -> Result<Config, LumenStatus> { fn to_config(self) -> Result<Config, PunktfunkStatus> {
let role = match self.role { let role = match self.role {
0 => Role::Host, 0 => Role::Host,
1 => Role::Client, 1 => Role::Client,
_ => return Err(LumenStatus::InvalidArg), _ => return Err(PunktfunkStatus::InvalidArg),
}; };
let phase = match self.phase { let phase = match self.phase {
1 => ProtocolPhase::P1GameStream, 1 => ProtocolPhase::P1GameStream,
2 => ProtocolPhase::P2Lumen, 2 => ProtocolPhase::P2Punktfunk,
_ => return Err(LumenStatus::InvalidArg), _ => return Err(PunktfunkStatus::InvalidArg),
}; };
// Range-check before narrowing: a `300` fec_percent or `65600` block size must be // Range-check before narrowing: a `300` fec_percent or `65600` block size must be
// rejected, not silently truncated to a valid-looking value. // rejected, not silently truncated to a valid-looking value.
let scheme = u8::try_from(self.fec_scheme) let scheme = u8::try_from(self.fec_scheme)
.ok() .ok()
.and_then(FecScheme::from_u8) .and_then(FecScheme::from_u8)
.ok_or(LumenStatus::InvalidArg)?; .ok_or(PunktfunkStatus::InvalidArg)?;
let fec_percent = u8::try_from(self.fec_percent).map_err(|_| LumenStatus::InvalidArg)?; let fec_percent =
u8::try_from(self.fec_percent).map_err(|_| PunktfunkStatus::InvalidArg)?;
let max_data_per_block = 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 { let cfg = Config {
role, role,
phase, 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) /// guard *before* reading the whole struct: a caller compiled against a smaller (older)
/// layout is rejected rather than causing an out-of-bounds read. /// layout is rejected rather than causing an out-of-bounds read.
/// ///
/// # Safety /// # Safety
/// `cfg` must either be null or point to at least its own declared `struct_size` bytes. /// `cfg` must either be null or point to at least its own declared `struct_size` bytes.
unsafe fn config_from_ptr(cfg: *const LumenConfig) -> Result<Config, LumenStatus> { unsafe fn config_from_ptr(cfg: *const PunktfunkConfig) -> Result<Config, PunktfunkStatus> {
if cfg.is_null() { 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. // 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; let declared = unsafe { std::ptr::addr_of!((*cfg).struct_size).read_unaligned() } as usize;
if declared < std::mem::size_of::<LumenConfig>() { if declared < std::mem::size_of::<PunktfunkConfig>() {
return Err(LumenStatus::InvalidArg); return Err(PunktfunkStatus::InvalidArg);
} }
unsafe { *cfg }.to_config() unsafe { *cfg }.to_config()
} }
/// A reassembled access unit. `data`/`len` borrow session-owned memory valid until the /// 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)] #[repr(C)]
pub struct LumenFrame { pub struct PunktfunkFrame {
pub data: *const u8, pub data: *const u8,
pub len: usize, pub len: usize,
pub frame_index: u32, pub frame_index: u32,
@@ -128,7 +129,7 @@ pub struct LumenFrame {
/// Snapshot of session counters. /// Snapshot of session counters.
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy, Default)] #[derive(Clone, Copy, Default)]
pub struct LumenStats { pub struct PunktfunkStats {
pub frames_submitted: u64, pub frames_submitted: u64,
pub frames_completed: u64, pub frames_completed: u64,
pub frames_dropped: u64, pub frames_dropped: u64,
@@ -140,9 +141,9 @@ pub struct LumenStats {
pub bytes_received: u64, pub bytes_received: u64,
} }
impl From<Stats> for LumenStats { impl From<Stats> for PunktfunkStats {
fn from(s: Stats) -> Self { fn from(s: Stats) -> Self {
LumenStats { PunktfunkStats {
frames_submitted: s.frames_submitted, frames_submitted: s.frames_submitted,
frames_completed: s.frames_completed, frames_completed: s.frames_completed,
frames_dropped: s.frames_dropped, frames_dropped: s.frames_dropped,
@@ -156,16 +157,16 @@ impl From<Stats> for LumenStats {
} }
} }
/// Host-side callback invoked for each input event drained by `lumen_host_poll_input`. /// Host-side callback invoked for each input event drained by `punktfunk_host_poll_input`.
pub type LumenInputCb = extern "C" fn(event: *const InputEvent, user: *mut c_void); pub type PunktfunkInputCb = extern "C" fn(event: *const InputEvent, user: *mut c_void);
#[inline] #[inline]
fn guard<F: FnOnce() -> LumenStatus>(f: F) -> LumenStatus { fn guard<F: FnOnce() -> PunktfunkStatus>(f: F) -> PunktfunkStatus {
std::panic::catch_unwind(AssertUnwindSafe(f)).unwrap_or(LumenStatus::Panic) std::panic::catch_unwind(AssertUnwindSafe(f)).unwrap_or(PunktfunkStatus::Panic)
} }
fn new_handle(session: Session) -> *mut LumenSession { fn new_handle(session: Session) -> *mut PunktfunkSession {
Box::into_raw(Box::new(LumenSession { Box::into_raw(Box::new(PunktfunkSession {
inner: session, inner: session,
last_frame: None, last_frame: None,
input_cb: 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. /// Current ABI version. Mismatch with [`crate::ABI_VERSION`] means incompatible core.
#[no_mangle] #[no_mangle]
pub extern "C" fn lumen_abi_version() -> u32 { pub extern "C" fn punktfunk_abi_version() -> u32 {
crate::ABI_VERSION crate::ABI_VERSION
} }
@@ -184,11 +185,11 @@ pub extern "C" fn lumen_abi_version() -> u32 {
/// # Safety /// # Safety
/// `cfg`, `local`, `peer` must be valid pointers; the strings must be NUL-terminated. /// `cfg`, `local`, `peer` must be valid pointers; the strings must be NUL-terminated.
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn lumen_session_new( pub unsafe extern "C" fn punktfunk_session_new(
cfg: *const LumenConfig, cfg: *const PunktfunkConfig,
local: *const c_char, local: *const c_char,
peer: *const c_char, peer: *const c_char,
) -> *mut LumenSession { ) -> *mut PunktfunkSession {
let result = std::panic::catch_unwind(AssertUnwindSafe(|| { let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
if cfg.is_null() || local.is_null() || peer.is_null() { if cfg.is_null() || local.is_null() || peer.is_null() {
return ptr::null_mut(); return ptr::null_mut();
@@ -223,16 +224,16 @@ pub unsafe extern "C" fn lumen_session_new(
/// # Safety /// # Safety
/// All four pointers must be valid; the two out-params receive owned handles. /// All four pointers must be valid; the two out-params receive owned handles.
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn lumen_test_loopback_pair( pub unsafe extern "C" fn punktfunk_test_loopback_pair(
host_cfg: *const LumenConfig, host_cfg: *const PunktfunkConfig,
client_cfg: *const LumenConfig, client_cfg: *const PunktfunkConfig,
out_host: *mut *mut LumenSession, out_host: *mut *mut PunktfunkSession,
out_client: *mut *mut LumenSession, out_client: *mut *mut PunktfunkSession,
) -> LumenStatus { ) -> PunktfunkStatus {
guard(|| { guard(|| {
if host_cfg.is_null() || client_cfg.is_null() || out_host.is_null() || out_client.is_null() 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) } { let hconf = match unsafe { config_from_ptr(host_cfg) } {
Ok(c) => c, Ok(c) => c,
@@ -255,16 +256,16 @@ pub unsafe extern "C" fn lumen_test_loopback_pair(
*out_host = new_handle(hs); *out_host = new_handle(hs);
*out_client = new_handle(cs); *out_client = new_handle(cs);
} }
LumenStatus::Ok PunktfunkStatus::Ok
}) })
} }
/// Free a session handle. Safe to call with NULL. /// Free a session handle. Safe to call with NULL.
/// ///
/// # Safety /// # 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] #[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() { if !s.is_null() {
drop(unsafe { Box::from_raw(s) }); drop(unsafe { Box::from_raw(s) });
} }
@@ -275,20 +276,20 @@ pub unsafe extern "C" fn lumen_session_free(s: *mut LumenSession) {
/// # Safety /// # Safety
/// `s` is a valid host handle; `data` points to `len` readable bytes (or `len == 0`). /// `s` is a valid host handle; `data` points to `len` readable bytes (or `len == 0`).
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn lumen_host_submit_frame( pub unsafe extern "C" fn punktfunk_host_submit_frame(
s: *mut LumenSession, s: *mut PunktfunkSession,
data: *const u8, data: *const u8,
len: usize, len: usize,
pts_ns: u64, pts_ns: u64,
flags: u32, flags: u32,
) -> LumenStatus { ) -> PunktfunkStatus {
guard(|| { guard(|| {
let s = match unsafe { s.as_mut() } { let s = match unsafe { s.as_mut() } {
Some(s) => s, Some(s) => s,
None => return LumenStatus::NullPointer, None => return PunktfunkStatus::NullPointer,
}; };
if data.is_null() && len != 0 { if data.is_null() && len != 0 {
return LumenStatus::NullPointer; return PunktfunkStatus::NullPointer;
} }
let slice = if len == 0 { 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) } unsafe { std::slice::from_raw_parts(data, len) }
}; };
match s.inner.submit_frame(slice, pts_ns, flags) { match s.inner.submit_frame(slice, pts_ns, flags) {
Ok(()) => LumenStatus::Ok, Ok(()) => PunktfunkStatus::Ok,
Err(e) => e.status(), 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. /// when nothing is ready yet. On `Ok`, `*out` borrows session memory until the next poll.
/// ///
/// # Safety /// # 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] #[no_mangle]
pub unsafe extern "C" fn lumen_client_poll_frame( pub unsafe extern "C" fn punktfunk_client_poll_frame(
s: *mut LumenSession, s: *mut PunktfunkSession,
out: *mut LumenFrame, out: *mut PunktfunkFrame,
) -> LumenStatus { ) -> PunktfunkStatus {
guard(|| { guard(|| {
let s = match unsafe { s.as_mut() } { let s = match unsafe { s.as_mut() } {
Some(s) => s, Some(s) => s,
None => return LumenStatus::NullPointer, None => return PunktfunkStatus::NullPointer,
}; };
if out.is_null() { if out.is_null() {
return LumenStatus::NullPointer; return PunktfunkStatus::NullPointer;
} }
match s.inner.poll_frame() { match s.inner.poll_frame() {
Ok(frame) => { Ok(frame) => {
s.last_frame = Some(frame); s.last_frame = Some(frame);
let f = s.last_frame.as_ref().unwrap(); let f = s.last_frame.as_ref().unwrap();
unsafe { unsafe {
*out = LumenFrame { *out = PunktfunkFrame {
data: f.data.as_ptr(), data: f.data.as_ptr(),
len: f.data.len(), len: f.data.len(),
frame_index: f.frame_index, frame_index: f.frame_index,
@@ -333,7 +334,7 @@ pub unsafe extern "C" fn lumen_client_poll_frame(
flags: f.flags, flags: f.flags,
}; };
} }
LumenStatus::Ok PunktfunkStatus::Ok
} }
Err(e) => e.status(), Err(e) => e.status(),
} }
@@ -345,60 +346,60 @@ pub unsafe extern "C" fn lumen_client_poll_frame(
/// # Safety /// # Safety
/// `s` is a valid client handle; `ev` points to a valid [`InputEvent`]. /// `s` is a valid client handle; `ev` points to a valid [`InputEvent`].
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn lumen_send_input( pub unsafe extern "C" fn punktfunk_send_input(
s: *mut LumenSession, s: *mut PunktfunkSession,
ev: *const InputEvent, ev: *const InputEvent,
) -> LumenStatus { ) -> PunktfunkStatus {
guard(|| { guard(|| {
let s = match unsafe { s.as_mut() } { let s = match unsafe { s.as_mut() } {
Some(s) => s, Some(s) => s,
None => return LumenStatus::NullPointer, None => return PunktfunkStatus::NullPointer,
}; };
let ev = match unsafe { ev.as_ref() } { let ev = match unsafe { ev.as_ref() } {
Some(e) => e, Some(e) => e,
None => return LumenStatus::NullPointer, None => return PunktfunkStatus::NullPointer,
}; };
match s.inner.send_input(ev) { match s.inner.send_input(ev) {
Ok(()) => LumenStatus::Ok, Ok(()) => PunktfunkStatus::Ok,
Err(e) => e.status(), Err(e) => e.status(),
} }
}) })
} }
/// Register the host-side input callback (pass a NULL fn pointer to clear). The callback /// 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 /// # Safety
/// `s` is a valid host handle; `user` is passed back verbatim to `cb`. /// `s` is a valid host handle; `user` is passed back verbatim to `cb`.
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn lumen_set_input_callback( pub unsafe extern "C" fn punktfunk_set_input_callback(
s: *mut LumenSession, s: *mut PunktfunkSession,
// Written as an explicit `Option<fn>` (not the `LumenInputCb` alias) so cbindgen // Written as an explicit `Option<fn>` (not the `PunktfunkInputCb` alias) so cbindgen
// emits a nullable C function pointer rather than an opaque wrapper struct. // emits a nullable C function pointer rather than an opaque wrapper struct.
cb: Option<extern "C" fn(event: *const InputEvent, user: *mut c_void)>, cb: Option<extern "C" fn(event: *const InputEvent, user: *mut c_void)>,
user: *mut c_void, user: *mut c_void,
) -> LumenStatus { ) -> PunktfunkStatus {
guard(|| { guard(|| {
let s = match unsafe { s.as_mut() } { let s = match unsafe { s.as_mut() } {
Some(s) => s, Some(s) => s,
None => return LumenStatus::NullPointer, None => return PunktfunkStatus::NullPointer,
}; };
s.input_cb = cb.map(|c| (c, user)); s.input_cb = cb.map(|c| (c, user));
LumenStatus::Ok PunktfunkStatus::Ok
}) })
} }
/// Host: drain all pending input events, invoking the registered callback for each. /// 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 /// # Safety
/// `s` is a valid host handle. /// `s` is a valid host handle.
#[no_mangle] #[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 r = std::panic::catch_unwind(AssertUnwindSafe(|| {
let s = match unsafe { s.as_mut() } { let s = match unsafe { s.as_mut() } {
Some(s) => s, Some(s) => s,
None => return LumenStatus::NullPointer as i32, None => return PunktfunkStatus::NullPointer as i32,
}; };
let cb = s.input_cb; let cb = s.input_cb;
let mut count = 0i32; let mut count = 0i32;
@@ -416,39 +417,39 @@ pub unsafe extern "C" fn lumen_host_poll_input(s: *mut LumenSession) -> i32 {
} }
count count
})); }));
r.unwrap_or(LumenStatus::Panic as i32) r.unwrap_or(PunktfunkStatus::Panic as i32)
} }
/// Copy session counters into `*out`. /// Copy session counters into `*out`.
/// ///
/// # Safety /// # 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] #[no_mangle]
pub unsafe extern "C" fn lumen_get_stats( pub unsafe extern "C" fn punktfunk_get_stats(
s: *mut LumenSession, s: *mut PunktfunkSession,
out: *mut LumenStats, out: *mut PunktfunkStats,
) -> LumenStatus { ) -> PunktfunkStatus {
guard(|| { guard(|| {
let s = match unsafe { s.as_ref() } { let s = match unsafe { s.as_ref() } {
Some(s) => s, Some(s) => s,
None => return LumenStatus::NullPointer, None => return PunktfunkStatus::NullPointer,
}; };
if out.is_null() { if out.is_null() {
return LumenStatus::NullPointer; return PunktfunkStatus::NullPointer;
} }
let stats = s.inner.stats(); let stats = s.inner.stats();
unsafe { *out = LumenStats::from(stats) }; unsafe { *out = PunktfunkStats::from(stats) };
LumenStatus::Ok 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 // 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). /// pumped on internal threads).
/// ///
/// Thread contract: each plane (video `next_au`, audio `next_audio`, rumble `next_rumble`) /// 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 /// take shared references internally (per-plane mutexed borrow slots), so cross-plane
/// concurrency is sound — never two threads on the *same* plane. /// concurrency is sound — never two threads on the *same* plane.
#[cfg(feature = "quic")] #[cfg(feature = "quic")]
pub struct LumenConnection { pub struct PunktfunkConnection {
inner: crate::client::NativeClient, inner: crate::client::NativeClient,
/// Backs the pointer returned by the last `lumen_connection_next_au` (borrow-until-next-call). /// Backs the pointer returned by the last `punktfunk_connection_next_au` (borrow-until-next-call).
last: std::sync::Mutex<Option<crate::session::Frame>>, last: std::sync::Mutex<Option<crate::session::Frame>>,
/// Same, for `lumen_connection_next_audio` (independent of the video slot). /// Same, for `punktfunk_connection_next_audio` (independent of the video slot).
last_audio: std::sync::Mutex<Option<crate::client::AudioPacket>>, last_audio: std::sync::Mutex<Option<crate::client::AudioPacket>>,
} }
/// Connect to a `lumen/1` host and start a session at `width`x`height`@`refresh_hz`. /// Connect to a `punktfunk/1` host and start a session at `width`x`height`@`refresh_hz`.
/// Blocks up to `timeout_ms` for the handshake. Returns NULL on failure. /// 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 /// 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. /// `pin_sha256`/`observed_sha256_out` are each NULL or valid for 32 bytes.
#[cfg(feature = "quic")] #[cfg(feature = "quic")]
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn lumen_connect( pub unsafe extern "C" fn punktfunk_connect(
host: *const std::os::raw::c_char, host: *const std::os::raw::c_char,
port: u16, port: u16,
width: u32, width: u32,
@@ -486,7 +487,7 @@ pub unsafe extern "C" fn lumen_connect(
pin_sha256: *const u8, pin_sha256: *const u8,
observed_sha256_out: *mut u8, observed_sha256_out: *mut u8,
timeout_ms: u32, timeout_ms: u32,
) -> *mut LumenConnection { ) -> *mut PunktfunkConnection {
let r = std::panic::catch_unwind(AssertUnwindSafe(|| { let r = std::panic::catch_unwind(AssertUnwindSafe(|| {
if host.is_null() { if host.is_null() {
return std::ptr::null_mut(); return std::ptr::null_mut();
@@ -521,7 +522,7 @@ pub unsafe extern "C" fn lumen_connect(
.copy_from_slice(&c.host_fingerprint); .copy_from_slice(&c.host_fingerprint);
} }
} }
Box::into_raw(Box::new(LumenConnection { Box::into_raw(Box::new(PunktfunkConnection {
inner: c, inner: c,
last: std::sync::Mutex::new(None), last: std::sync::Mutex::new(None),
last_audio: 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 /// 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 /// On `Ok`, `*out` borrows connection memory **until the next `next_au` call** on this
/// handle (the audio/rumble planes do not invalidate it). /// 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. /// it may run concurrently with one audio-pulling and one rumble-pulling thread.
#[cfg(feature = "quic")] #[cfg(feature = "quic")]
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn lumen_connection_next_au( pub unsafe extern "C" fn punktfunk_connection_next_au(
c: *mut LumenConnection, c: *mut PunktfunkConnection,
out: *mut LumenFrame, out: *mut PunktfunkFrame,
timeout_ms: u32, timeout_ms: u32,
) -> LumenStatus { ) -> PunktfunkStatus {
guard(|| { guard(|| {
// Shared reference only: video and audio threads must never alias a `&mut`. // Shared reference only: video and audio threads must never alias a `&mut`.
let c = match unsafe { c.as_ref() } { let c = match unsafe { c.as_ref() } {
Some(c) => c, Some(c) => c,
None => return LumenStatus::NullPointer, None => return PunktfunkStatus::NullPointer,
}; };
if out.is_null() { if out.is_null() {
return LumenStatus::NullPointer; return PunktfunkStatus::NullPointer;
} }
match c match c
.inner .inner
@@ -566,7 +567,7 @@ pub unsafe extern "C" fn lumen_connection_next_au(
*slot = Some(frame); *slot = Some(frame);
let f = slot.as_ref().unwrap(); let f = slot.as_ref().unwrap();
unsafe { unsafe {
*out = LumenFrame { *out = PunktfunkFrame {
data: f.data.as_ptr(), data: f.data.as_ptr(),
len: f.data.len(), len: f.data.len(),
frame_index: f.frame_index, frame_index: f.frame_index,
@@ -574,18 +575,18 @@ pub unsafe extern "C" fn lumen_connection_next_au(
flags: f.flags, flags: f.flags,
}; };
} }
LumenStatus::Ok PunktfunkStatus::Ok
} }
Err(e) => e.status(), Err(e) => e.status(),
} }
}) })
} }
/// One Opus audio packet pulled off a `lumen/1` connection (48 kHz stereo, 5 ms frames). /// One Opus audio packet pulled off a `punktfunk/1` connection (48 kHz stereo, 5 ms frames).
/// `data` borrows connection memory until the next `lumen_connection_next_audio` call. /// `data` borrows connection memory until the next `punktfunk_connection_next_audio` call.
#[cfg(feature = "quic")] #[cfg(feature = "quic")]
#[repr(C)] #[repr(C)]
pub struct LumenAudioPacket { pub struct PunktfunkAudioPacket {
pub data: *const u8, pub data: *const u8,
pub len: usize, pub len: usize,
pub seq: u32, pub seq: u32,
@@ -593,7 +594,7 @@ pub struct LumenAudioPacket {
} }
/// Pull the next Opus audio packet, waiting up to `timeout_ms`. Returns /// 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 /// 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 /// handle (independent of the video slot). Drain from a dedicated audio thread — packets
/// arrive every 5 ms and the internal queue holds 320 ms. /// 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. /// it may run concurrently with the video/rumble pullers.
#[cfg(feature = "quic")] #[cfg(feature = "quic")]
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn lumen_connection_next_audio( pub unsafe extern "C" fn punktfunk_connection_next_audio(
c: *mut LumenConnection, c: *mut PunktfunkConnection,
out: *mut LumenAudioPacket, out: *mut PunktfunkAudioPacket,
timeout_ms: u32, timeout_ms: u32,
) -> LumenStatus { ) -> PunktfunkStatus {
guard(|| { guard(|| {
let c = match unsafe { c.as_ref() } { let c = match unsafe { c.as_ref() } {
Some(c) => c, Some(c) => c,
None => return LumenStatus::NullPointer, None => return PunktfunkStatus::NullPointer,
}; };
if out.is_null() { if out.is_null() {
return LumenStatus::NullPointer; return PunktfunkStatus::NullPointer;
} }
match c match c
.inner .inner
@@ -625,14 +626,14 @@ pub unsafe extern "C" fn lumen_connection_next_audio(
*slot = Some(pkt); *slot = Some(pkt);
let p = slot.as_ref().unwrap(); let p = slot.as_ref().unwrap();
unsafe { unsafe {
*out = LumenAudioPacket { *out = PunktfunkAudioPacket {
data: p.data.as_ptr(), data: p.data.as_ptr(),
len: p.data.len(), len: p.data.len(),
seq: p.seq, seq: p.seq,
pts_ns: p.pts_ns, pts_ns: p.pts_ns,
}; };
} }
LumenStatus::Ok PunktfunkStatus::Ok
} }
Err(e) => e.status(), 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 /// 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. /// 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 /// # Safety
/// `c` is a valid connection handle; out pointers are writable (NULLs are skipped). At /// `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. /// most one thread pulls rumble — it may run concurrently with the video/audio pullers.
#[cfg(feature = "quic")] #[cfg(feature = "quic")]
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn lumen_connection_next_rumble( pub unsafe extern "C" fn punktfunk_connection_next_rumble(
c: *mut LumenConnection, c: *mut PunktfunkConnection,
pad: *mut u16, pad: *mut u16,
low: *mut u16, low: *mut u16,
high: *mut u16, high: *mut u16,
timeout_ms: u32, timeout_ms: u32,
) -> LumenStatus { ) -> PunktfunkStatus {
guard(|| { guard(|| {
let c = match unsafe { c.as_ref() } { let c = match unsafe { c.as_ref() } {
Some(c) => c, Some(c) => c,
None => return LumenStatus::NullPointer, None => return PunktfunkStatus::NullPointer,
}; };
match c match c
.inner .inner
@@ -676,7 +677,7 @@ pub unsafe extern "C" fn lumen_connection_next_rumble(
*high = h; *high = h;
} }
} }
LumenStatus::Ok PunktfunkStatus::Ok
} }
Err(e) => e.status(), 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`]. /// `c` is a valid connection handle; `ev` points to a valid [`InputEvent`].
#[cfg(feature = "quic")] #[cfg(feature = "quic")]
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn lumen_connection_send_input( pub unsafe extern "C" fn punktfunk_connection_send_input(
c: *mut LumenConnection, c: *mut PunktfunkConnection,
ev: *const InputEvent, ev: *const InputEvent,
) -> LumenStatus { ) -> PunktfunkStatus {
guard(|| { guard(|| {
let c = match unsafe { c.as_ref() } { let c = match unsafe { c.as_ref() } {
Some(c) => c, Some(c) => c,
None => return LumenStatus::NullPointer, None => return PunktfunkStatus::NullPointer,
}; };
let ev = match unsafe { ev.as_ref() } { let ev = match unsafe { ev.as_ref() } {
Some(e) => e, Some(e) => e,
None => return LumenStatus::NullPointer, None => return PunktfunkStatus::NullPointer,
}; };
match c.inner.send_input(ev) { match c.inner.send_input(ev) {
Ok(()) => LumenStatus::Ok, Ok(()) => PunktfunkStatus::Ok,
Err(e) => e.status(), 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). /// `c` is a valid connection handle; out pointers are writable (NULLs are skipped).
#[cfg(feature = "quic")] #[cfg(feature = "quic")]
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn lumen_connection_mode( pub unsafe extern "C" fn punktfunk_connection_mode(
c: *const LumenConnection, c: *const PunktfunkConnection,
width: *mut u32, width: *mut u32,
height: *mut u32, height: *mut u32,
refresh_hz: *mut u32, refresh_hz: *mut u32,
) -> LumenStatus { ) -> PunktfunkStatus {
guard(|| { guard(|| {
let c = match unsafe { c.as_ref() } { let c = match unsafe { c.as_ref() } {
Some(c) => c, Some(c) => c,
None => return LumenStatus::NullPointer, None => return PunktfunkStatus::NullPointer,
}; };
unsafe { unsafe {
if !width.is_null() { if !width.is_null() {
@@ -737,17 +738,17 @@ pub unsafe extern "C" fn lumen_connection_mode(
*refresh_hz = c.inner.mode.refresh_hz; *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. /// Close the connection and free the handle (joins the internal threads). NULL is a no-op.
/// ///
/// # Safety /// # 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")] #[cfg(feature = "quic")]
#[no_mangle] #[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() { if !c.is_null() {
drop(unsafe { Box::from_raw(c) }); drop(unsafe { Box::from_raw(c) });
} }
@@ -1,10 +1,10 @@
//! The embeddable `lumen/1` client connector (M4 groundwork), behind the `quic` feature. //! The embeddable `punktfunk/1` client connector (M4 groundwork), behind the `quic` feature.
//! //!
//! [`NativeClient::connect`] runs the full client side of the protocol — QUIC handshake //! [`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 //! ([`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, //! 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, …) //! 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. //! Rust-native consumer of the same flow.
//! //!
//! Threading: one worker thread owns a tokio runtime (QUIC control plane only — design //! 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. //! channel. All methods are safe to call from any single embedder thread.
use crate::config::{Mode, Role}; use crate::config::{Mode, Role};
use crate::error::{LumenError, Result}; use crate::error::{PunktfunkError, Result};
use crate::input::InputEvent; use crate::input::InputEvent;
use crate::quic::{endpoint, io, Hello, Start, Welcome}; use crate::quic::{endpoint, io, Hello, Start, Welcome};
use crate::session::{Frame, Session}; use crate::session::{Frame, Session};
@@ -60,11 +60,11 @@ pub struct NativeClient {
} }
impl 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. /// handshake completes or `timeout` elapses.
/// ///
/// `pin`: expected SHA-256 of the host's certificate. `Some` and the host presents /// `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. /// first use; check [`NativeClient::host_fingerprint`] after connecting.
pub fn connect( pub fn connect(
host: &str, host: &str,
@@ -83,7 +83,7 @@ impl NativeClient {
let host = host.to_string(); let host = host.to_string();
let shutdown_w = shutdown.clone(); let shutdown_w = shutdown.clone();
let worker = std::thread::Builder::new() let worker = std::thread::Builder::new()
.name("lumen-client".into()) .name("punktfunk-client".into())
.spawn(move || { .spawn(move || {
let rt = match tokio::runtime::Builder::new_multi_thread() let rt = match tokio::runtime::Builder::new_multi_thread()
.worker_threads(2) .worker_threads(2)
@@ -92,7 +92,7 @@ impl NativeClient {
{ {
Ok(rt) => rt, Ok(rt) => rt,
Err(e) => { Err(e) => {
let _ = ready_tx.send(Err(LumenError::Io(e))); let _ = ready_tx.send(Err(PunktfunkError::Io(e)));
return; return;
} }
}; };
@@ -109,14 +109,14 @@ impl NativeClient {
shutdown: shutdown_w, shutdown: shutdown_w,
})); }));
}) })
.map_err(LumenError::Io)?; .map_err(PunktfunkError::Io)?;
let (negotiated, fingerprint) = match ready_rx.recv_timeout(timeout) { let (negotiated, fingerprint) = match ready_rx.recv_timeout(timeout) {
Ok(Ok(t)) => t, Ok(Ok(t)) => t,
Ok(Err(e)) => return Err(e), Ok(Err(e)) => return Err(e),
Err(_) => { Err(_) => {
shutdown.store(true, Ordering::SeqCst); shutdown.store(true, Ordering::SeqCst);
return Err(LumenError::Timeout); return Err(PunktfunkError::Timeout);
} }
}; };
Ok(NativeClient { Ok(NativeClient {
@@ -131,8 +131,8 @@ impl NativeClient {
}) })
} }
/// Pull the next reassembled, FEC-recovered access unit; [`LumenError::NoFrame`] on /// Pull the next reassembled, FEC-recovered access unit; [`PunktfunkError::NoFrame`] on
/// timeout, [`LumenError::Closed`]-class errors once the session ended. /// timeout, [`PunktfunkError::Closed`]-class errors once the session ended.
/// ///
/// Plane concurrency: each pull method drains its own queue, so video, audio and /// 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 /// rumble may each be pulled from their own thread — but at most one thread per plane
@@ -141,19 +141,19 @@ impl NativeClient {
pub fn next_frame(&self, timeout: Duration) -> Result<Frame> { pub fn next_frame(&self, timeout: Duration) -> Result<Frame> {
match self.frames.recv_timeout(timeout) { match self.frames.recv_timeout(timeout) {
Ok(f) => Ok(f), Ok(f) => Ok(f),
Err(RecvTimeoutError::Timeout) => Err(LumenError::NoFrame), Err(RecvTimeoutError::Timeout) => Err(PunktfunkError::NoFrame),
Err(RecvTimeoutError::Disconnected) => Err(LumenError::Closed), Err(RecvTimeoutError::Disconnected) => Err(PunktfunkError::Closed),
} }
} }
/// Pull the next Opus audio packet; [`LumenError::NoFrame`] on timeout, /// Pull the next Opus audio packet; [`PunktfunkError::NoFrame`] on timeout,
/// [`LumenError::Closed`] once the session ended. Drain on a dedicated audio thread — /// [`PunktfunkError::Closed`] once the session ended. Drain on a dedicated audio thread —
/// packets arrive every 5 ms. /// packets arrive every 5 ms.
pub fn next_audio(&self, timeout: Duration) -> Result<AudioPacket> { pub fn next_audio(&self, timeout: Duration) -> Result<AudioPacket> {
match self.audio.recv_timeout(timeout) { match self.audio.recv_timeout(timeout) {
Ok(p) => Ok(p), Ok(p) => Ok(p),
Err(RecvTimeoutError::Timeout) => Err(LumenError::NoFrame), Err(RecvTimeoutError::Timeout) => Err(PunktfunkError::NoFrame),
Err(RecvTimeoutError::Disconnected) => Err(LumenError::Closed), Err(RecvTimeoutError::Disconnected) => Err(PunktfunkError::Closed),
} }
} }
@@ -162,14 +162,14 @@ impl NativeClient {
pub fn next_rumble(&self, timeout: Duration) -> Result<(u16, u16, u16)> { pub fn next_rumble(&self, timeout: Duration) -> Result<(u16, u16, u16)> {
match self.rumble.recv_timeout(timeout) { match self.rumble.recv_timeout(timeout) {
Ok(r) => Ok(r), Ok(r) => Ok(r),
Err(RecvTimeoutError::Timeout) => Err(LumenError::NoFrame), Err(RecvTimeoutError::Timeout) => Err(PunktfunkError::NoFrame),
Err(RecvTimeoutError::Disconnected) => Err(LumenError::Closed), Err(RecvTimeoutError::Disconnected) => Err(PunktfunkError::Closed),
} }
} }
/// Queue one input event for delivery as a QUIC datagram. /// Queue one input event for delivery as a QUIC datagram.
pub fn send_input(&self, ev: &InputEvent) -> Result<()> { 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 setup = async {
let remote: std::net::SocketAddr = format!("{host}:{port}") let remote: std::net::SocketAddr = format!("{host}:{port}")
.parse() .parse()
.map_err(|_| LumenError::InvalidArg("host:port"))?; .map_err(|_| PunktfunkError::InvalidArg("host:port"))?;
let (ep, observed) = endpoint::client_pinned(pin); 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 let conn = ep
.connect(remote, "lumen") .connect(remote, "punktfunk")
.map_err(|_| LumenError::InvalidArg("connect"))? .map_err(|_| PunktfunkError::InvalidArg("connect"))?
.await .await
.map_err(|e| { .map_err(|e| {
// A pin mismatch surfaces as a TLS failure; report it as a crypto error so // 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() let fp_mismatch = pin.is_some()
&& observed.lock().unwrap().map(|fp| Some(fp) != pin) == Some(true); && observed.lock().unwrap().map(|fp| Some(fp) != pin) == Some(true);
if fp_mismatch { if fp_mismatch {
LumenError::Crypto PunktfunkError::Crypto
} else { } 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 fingerprint = observed.lock().unwrap().unwrap_or([0u8; 32]);
let (mut send, mut recv) = conn let (mut send, mut recv) = conn
.open_bi() .open_bi()
.await .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( io::write_msg(
&mut send, &mut send,
@@ -264,7 +264,7 @@ async fn worker_main(args: WorkerArgs) {
let transport = let transport =
UdpTransport::connect(&format!("0.0.0.0:{udp_port}"), &host_udp.to_string())?; 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))?; 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 { let (conn, mut session, negotiated, fingerprint) = match setup.await {
@@ -328,7 +328,7 @@ async fn worker_main(args: WorkerArgs) {
Ok(frame) => { Ok(frame) => {
let _ = frame_tx.try_send(frame); let _ = frame_tx.try_send(frame);
} }
Err(LumenError::NoFrame) => { Err(PunktfunkError::NoFrame) => {
std::thread::sleep(Duration::from_micros(300)); std::thread::sleep(Duration::from_micros(300));
} }
Err(_) => break, Err(_) => break,
@@ -1,6 +1,6 @@
//! Session configuration and protocol/FEC parameters. //! 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 crate::packet::{CRYPTO_OVERHEAD, HEADER_LEN, MAX_DATAGRAM_BYTES};
use zeroize::Zeroize; use zeroize::Zeroize;
@@ -13,12 +13,12 @@ pub enum Role {
} }
/// Negotiated protocol generation. P1 is GameStream-compatible (GF(2⁸)); P2 is the /// 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)] #[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ProtocolPhase { pub enum ProtocolPhase {
P1GameStream = 1, P1GameStream = 1,
P2Lumen = 2, P2Punktfunk = 2,
} }
/// Erasure-coding field. Mirrors the on-wire `fec_scheme` tag. /// 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. /// is what keeps the receive-side parser's allocations bounded.
pub fn validate(&self) -> Result<()> { pub fn validate(&self) -> Result<()> {
if self.shard_payload == 0 || self.shard_payload % 2 != 0 { 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() { 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)", "shard_payload too large to fit a datagram (header + crypto overhead)",
)); ));
} }
if self.fec.max_data_per_block == 0 { 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 // The per-block total (data + recovery) must fit both the field ceiling and the
// u16 wire fields. // u16 wire fields.
let k = self.fec.max_data_per_block as usize; let k = self.fec.max_data_per_block as usize;
let total = k + self.fec.recovery_for(k); let total = k + self.fec.recovery_for(k);
if total > self.fec.scheme.max_total_shards() { 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", "max_data_per_block + recovery exceeds the FEC scheme's shard ceiling",
)); ));
} }
if self.max_frame_bytes == 0 { 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. // 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 total_data = self.max_frame_bytes.div_ceil(self.shard_payload).max(1);
let max_blocks = total_data.div_ceil(k).max(1); let max_blocks = total_data.div_ceil(k).max(1);
if max_blocks > u16::MAX as usize { 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)", "max_frame_bytes too large for this shard/block configuration (block count overflows u16)",
)); ));
} }
if self.encrypt && self.key == [0u8; 16] { 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)", "encrypt requires a non-zero session key (see crypto nonce-uniqueness contract)",
)); ));
} }
@@ -19,7 +19,7 @@
//! nonce. Note: this layer does not provide anti-replay — see `Session`. //! nonce. Note: this layer does not provide anti-replay — see `Session`.
use crate::config::Role; use crate::config::Role;
use crate::error::{LumenError, Result}; use crate::error::{PunktfunkError, Result};
use aes_gcm::aead::{Aead, KeyInit, Payload}; use aes_gcm::aead::{Aead, KeyInit, Payload};
use aes_gcm::{Aes128Gcm, Key, Nonce}; use aes_gcm::{Aes128Gcm, Key, Nonce};
@@ -57,7 +57,7 @@ impl SessionCrypto {
aad: &seq.to_be_bytes(), aad: &seq.to_be_bytes(),
}, },
) )
.map_err(|_| LumenError::Crypto) .map_err(|_| PunktfunkError::Crypto)
} }
/// Open `ciphertext || tag` for sequence `seq` (also bound as associated data). /// Open `ciphertext || tag` for sequence `seq` (also bound as associated data).
@@ -71,7 +71,7 @@ impl SessionCrypto {
aad: &seq.to_be_bytes(), aad: &seq.to_be_bytes(),
}, },
) )
.map_err(|_| LumenError::Crypto) .map_err(|_| PunktfunkError::Crypto)
} }
} }
@@ -2,9 +2,9 @@
use thiserror::Error; 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)] #[derive(Debug, Error)]
pub enum LumenError { pub enum PunktfunkError {
#[error("invalid argument: {0}")] #[error("invalid argument: {0}")]
InvalidArg(&'static str), InvalidArg(&'static str),
#[error("fec error: {0}")] #[error("fec error: {0}")]
@@ -25,13 +25,13 @@ pub enum LumenError {
Closed, Closed,
} }
pub type Result<T> = core::result::Result<T, LumenError>; pub type Result<T> = core::result::Result<T, PunktfunkError>;
/// Stable C ABI status codes. `Ok` is 0; all errors are negative so callers can /// 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. /// test `rc < 0`. Do not renumber existing variants — only append.
#[repr(i32)] #[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LumenStatus { pub enum PunktfunkStatus {
Ok = 0, Ok = 0,
InvalidArg = -1, InvalidArg = -1,
Fec = -2, Fec = -2,
@@ -46,19 +46,19 @@ pub enum LumenStatus {
Panic = -99, Panic = -99,
} }
impl LumenError { impl PunktfunkError {
/// Map to the C ABI status code. /// Map to the C ABI status code.
pub fn status(&self) -> LumenStatus { pub fn status(&self) -> PunktfunkStatus {
match self { match self {
LumenError::InvalidArg(_) => LumenStatus::InvalidArg, PunktfunkError::InvalidArg(_) => PunktfunkStatus::InvalidArg,
LumenError::Fec(_) => LumenStatus::Fec, PunktfunkError::Fec(_) => PunktfunkStatus::Fec,
LumenError::Crypto => LumenStatus::Crypto, PunktfunkError::Crypto => PunktfunkStatus::Crypto,
LumenError::BadPacket => LumenStatus::BadPacket, PunktfunkError::BadPacket => PunktfunkStatus::BadPacket,
LumenError::NoFrame => LumenStatus::NoFrame, PunktfunkError::NoFrame => PunktfunkStatus::NoFrame,
LumenError::Unsupported(_) => LumenStatus::Unsupported, PunktfunkError::Unsupported(_) => PunktfunkStatus::Unsupported,
LumenError::Io(_) => LumenStatus::Io, PunktfunkError::Io(_) => PunktfunkStatus::Io,
LumenError::Timeout => LumenStatus::Timeout, PunktfunkError::Timeout => PunktfunkStatus::Timeout,
LumenError::Closed => LumenStatus::Closed, PunktfunkError::Closed => PunktfunkStatus::Closed,
} }
} }
} }
@@ -85,7 +85,7 @@ impl InputKind {
} }
/// A single input event. `#[repr(C)]` — shared verbatim with the C ABI as /// A single input event. `#[repr(C)]` — shared verbatim with the C ABI as
/// `LumenInputEvent`. /// `PunktfunkInputEvent`.
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InputEvent { pub struct InputEvent {
@@ -1,6 +1,6 @@
//! # lumen-core //! # punktfunk-core
//! //!
//! The shared protocol / transport / FEC core for the lumen low-latency streaming //! The shared protocol / transport / FEC core for the punktfunk low-latency streaming
//! stack. It is compiled exactly once and linked by every host and client — directly //! 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. //! 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 //! - [`session`] — the host (submit frame → FEC → packetize → seal → send) and client
//! (recv → open → reorder → FEC recover → reassemble) state machines. //! (recv → open → reorder → FEC recover → reassemble) state machines.
//! - [`transport`] — pluggable packet I/O (in-process loopback for tests; UDP for real). //! - [`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 //! ## Threading contract
//! //!
@@ -40,10 +40,10 @@ pub mod stats;
pub mod transport; pub mod transport;
pub use config::{Config, FecConfig, FecScheme, Mode, ProtocolPhase, Role}; 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 session::{Frame, Session};
pub use stats::Stats; pub use stats::Stats;
/// Bump on any breaking change to the [C ABI](crate::abi). Mirrors /// 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; pub const ABI_VERSION: u32 = 1;
@@ -4,7 +4,7 @@
//! ## Wire layout //! ## Wire layout
//! //!
//! Each packet is a fixed [`PacketHeader`] followed by one FEC shard's payload. Fields //! 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. //! spec will pin byte order explicitly when we talk to non-LE peers.
//! //!
//! ## GameStream mapping (P1) //! ## GameStream mapping (P1)
@@ -16,15 +16,15 @@
//! concern (it also needs RTP framing + RTSP), this is the coherent internal format. //! concern (it also needs RTP framing + RTSP), this is the coherent internal format.
use crate::config::Config; use crate::config::Config;
use crate::error::{LumenError, Result}; use crate::error::{PunktfunkError, Result};
use crate::fec::ErasureCoder; use crate::fec::ErasureCoder;
use crate::session::Frame; use crate::session::Frame;
use crate::stats::StatsCounters; use crate::stats::StatsCounters;
use std::collections::{BTreeMap, HashMap, HashSet}; use std::collections::{BTreeMap, HashMap, HashSet};
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
/// Identifies a lumen video packet (vs. an input datagram, see [`crate::input`]). /// Identifies a punktfunk video packet (vs. an input datagram, see [`crate::input`]).
pub const LUMEN_MAGIC: u8 = 0xC9; pub const PUNKTFUNK_MAGIC: u8 = 0xC9;
// Frame flags (mirroring GameStream's FLAG_*). // Frame flags (mirroring GameStream's FLAG_*).
pub const FLAG_PIC: u8 = 0x1; 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 // 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. // the belt-and-suspenders for a frame larger than the negotiated maximum.
if payload > u16::MAX as usize { 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 { if block_count > u16::MAX as usize {
return Err(LumenError::Unsupported( return Err(PunktfunkError::Unsupported(
"frame too large: block count exceeds u16", "frame too large: block count exceeds u16",
)); ));
} }
@@ -144,7 +144,7 @@ impl Packetizer {
let recovery = coder.encode(&data_shards, recovery_count)?; let recovery = coder.encode(&data_shards, recovery_count)?;
let total_shards = block_data_count + recovery_count; let total_shards = block_data_count + recovery_count;
if total_shards > u16::MAX as usize { 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 { for shard_index in 0..total_shards {
@@ -177,7 +177,7 @@ impl Packetizer {
recovery_shards: recovery_count as u16, recovery_shards: recovery_count as u16,
shard_index: shard_index as u16, shard_index: shard_index as u16,
shard_bytes: payload as u16, shard_bytes: payload as u16,
magic: LUMEN_MAGIC, magic: PUNKTFUNK_MAGIC,
version: self.version, version: self.version,
fec_scheme: coder.scheme() as u8, fec_scheme: coder.scheme() as u8,
flags, flags,
@@ -309,7 +309,7 @@ impl Reassembler {
let drop = |stats: &StatsCounters| { let drop = |stats: &StatsCounters| {
StatsCounters::add(&stats.packets_dropped, 1); StatsCounters::add(&stats.packets_dropped, 1);
}; };
if hdr.magic != LUMEN_MAGIC if hdr.magic != PUNKTFUNK_MAGIC
|| shard_bytes != lim.shard_bytes || shard_bytes != lim.shard_bytes
|| pkt.len() < HEADER_LEN + shard_bytes || pkt.len() < HEADER_LEN + shard_bytes
|| data_shards == 0 || data_shards == 0
@@ -493,7 +493,7 @@ mod tests {
recovery_shards: 0, recovery_shards: 0,
shard_index: 0, shard_index: 0,
shard_bytes: 16, shard_bytes: 16,
magic: LUMEN_MAGIC, magic: PUNKTFUNK_MAGIC,
version: 1, version: 1,
fec_scheme: 0, fec_scheme: 0,
flags: FLAG_PIC, flags: FLAG_PIC,
@@ -1,6 +1,6 @@
//! `lumen/1` — the native control plane (M3), gated behind the `quic` feature. //! `punktfunk/1` — the native control plane (M3), gated behind the `quic` feature.
//! //!
//! GameStream is lumen's compatibility layer; this is the start of its own protocol. A QUIC //! GameStream is punktfunk's compatibility layer; this is the start of its own protocol. A QUIC
//! connection (quinn, tokio — control plane only, never the per-frame path) carries a //! connection (quinn, tokio — control plane only, never the per-frame path) carries a
//! length-prefixed binary handshake on one bidirectional stream: //! length-prefixed binary handshake on one bidirectional stream:
//! //!
@@ -23,10 +23,10 @@
//! All integers little-endian; every message is `u16 length || payload`. //! All integers little-endian; every message is `u16 length || payload`.
use crate::config::{Config, FecConfig, FecScheme, Mode, ProtocolPhase, Role}; 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. /// 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 /// `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). /// virtual output at exactly this size/refresh — native resolution end to end).
@@ -71,7 +71,7 @@ impl Hello {
pub fn decode(b: &[u8]) -> Result<Hello> { pub fn decode(b: &[u8]) -> Result<Hello> {
if b.len() < 20 || &b[0..4] != MAGIC { 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]]); let u32at = |o: usize| u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]);
Ok(Hello { Ok(Hello {
@@ -113,7 +113,7 @@ impl Welcome {
// scheme[22] pct[23] max_data[24..26] shard[26..28] encrypt[28] key[29..45] // scheme[22] pct[23] max_data[24..26] shard[26..28] encrypt[28] key[29..45]
// salt[45..49] frames[49..53]. // salt[45..49] frames[49..53].
if b.len() < 53 || &b[0..4] != MAGIC { 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 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]]); let u16at = |o: usize| u16::from_le_bytes([b[o], b[o + 1]]);
@@ -169,7 +169,7 @@ impl Start {
pub fn decode(b: &[u8]) -> Result<Start> { pub fn decode(b: &[u8]) -> Result<Start> {
if b.len() < 6 || &b[0..4] != MAGIC { if b.len() < 6 || &b[0..4] != MAGIC {
return Err(LumenError::InvalidArg("bad Start")); return Err(PunktfunkError::InvalidArg("bad Start"));
} }
Ok(Start { Ok(Start {
client_udp_port: u16::from_le_bytes([b[4], b[5]]), 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 /// 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). /// persist an identity and use [`server_with_identity`] so clients can pin it).
pub fn server(addr: std::net::SocketAddr) -> anyhow_result::Result<quinn::Endpoint> { pub fn server(addr: std::net::SocketAddr) -> anyhow_result::Result<quinn::Endpoint> {
let cert = rcgen::generate_simple_self_signed(vec!["lumen".into()]) let cert = rcgen::generate_simple_self_signed(vec!["punktfunk".into()])
.map_err(|e| anyhow_result::Error::msg(format!("self-signed cert: {e}")))?; .map_err(|e| anyhow_result::Error::msg(format!("self-signed cert: {e}")))?;
let cert_der = rustls::pki_types::CertificateDer::from(cert.cert); let cert_der = rustls::pki_types::CertificateDer::from(cert.cert);
let key_der = rustls::pki_types::PrivatePkcs8KeyDer::from(cert.key_pair.serialize_der()); let key_der = rustls::pki_types::PrivatePkcs8KeyDer::from(cert.key_pair.serialize_der());
@@ -351,7 +351,7 @@ pub mod endpoint {
(ep, observed) (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 mod anyhow_result {
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)] #[derive(Debug)]
@@ -10,7 +10,7 @@
use crate::config::{Config, Role}; use crate::config::{Config, Role};
use crate::crypto::SessionCrypto; use crate::crypto::SessionCrypto;
use crate::error::{LumenError, Result}; use crate::error::{PunktfunkError, Result};
use crate::fec::{coder_for, ErasureCoder}; use crate::fec::{coder_for, ErasureCoder};
use crate::input::InputEvent; use crate::input::InputEvent;
use crate::packet::{Packetizer, Reassembler, ReassemblerLimits}; 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 /// 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. /// 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 /// Video replays are largely absorbed by the reassembler's per-frame dedup, but replayed
@@ -96,7 +96,7 @@ impl Session {
match &self.crypto { match &self.crypto {
Some(c) => { Some(c) => {
if wire.len() < 8 { if wire.len() < 8 {
return Err(LumenError::BadPacket); return Err(PunktfunkError::BadPacket);
} }
let seq = u64::from_be_bytes(wire[..8].try_into().unwrap()); let seq = u64::from_be_bytes(wire[..8].try_into().unwrap());
c.open(seq, &wire[8..]) c.open(seq, &wire[8..])
@@ -110,7 +110,7 @@ impl Session {
/// Host: FEC-protect, packetize, seal, and send one encoded access unit. /// 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<()> { pub fn submit_frame(&mut self, data: &[u8], pts_ns: u64, user_flags: u32) -> Result<()> {
if self.config.role != Role::Host { if self.config.role != Role::Host {
return Err(LumenError::InvalidArg( return Err(PunktfunkError::InvalidArg(
"submit_frame called on a client session", "submit_frame called on a client session",
)); ));
} }
@@ -130,7 +130,7 @@ impl Session {
/// Host: drain one pending input event from the client, if any. /// Host: drain one pending input event from the client, if any.
pub fn poll_input(&mut self) -> Result<Option<InputEvent>> { pub fn poll_input(&mut self) -> Result<Option<InputEvent>> {
if self.config.role != Role::Host { if self.config.role != Role::Host {
return Err(LumenError::InvalidArg( return Err(PunktfunkError::InvalidArg(
"poll_input called on a client session", "poll_input called on a client session",
)); ));
} }
@@ -151,17 +151,17 @@ impl Session {
// -- Client path ------------------------------------------------------ // -- Client path ------------------------------------------------------
/// Client: drain the transport until a whole access unit is recovered, or no more /// Client: drain the transport until a whole access unit is recovered, or no more
/// packets are pending ([`LumenError::NoFrame`]). /// packets are pending ([`PunktfunkError::NoFrame`]).
pub fn poll_frame(&mut self) -> Result<Frame> { pub fn poll_frame(&mut self) -> Result<Frame> {
if self.config.role != Role::Client { if self.config.role != Role::Client {
return Err(LumenError::InvalidArg( return Err(PunktfunkError::InvalidArg(
"poll_frame called on a host session", "poll_frame called on a host session",
)); ));
} }
loop { loop {
let wire = match self.transport.recv()? { let wire = match self.transport.recv()? {
Some(w) => w, Some(w) => w,
None => return Err(LumenError::NoFrame), None => return Err(PunktfunkError::NoFrame),
}; };
let pkt = match self.open_from_wire(&wire) { let pkt = match self.open_from_wire(&wire) {
Ok(p) => p, Ok(p) => p,
@@ -184,7 +184,7 @@ impl Session {
/// Client: serialize and send one input event to the host. /// Client: serialize and send one input event to the host.
pub fn send_input(&mut self, event: &InputEvent) -> Result<()> { pub fn send_input(&mut self, event: &InputEvent) -> Result<()> {
if self.config.role != Role::Client { if self.config.role != Role::Client {
return Err(LumenError::InvalidArg( return Err(PunktfunkError::InvalidArg(
"send_input called on a host session", "send_input called on a host session",
)); ));
} }
@@ -2,7 +2,7 @@
use std::sync::atomic::{AtomicU64, Ordering}; 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)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Stats { pub struct Stats {
pub frames_submitted: u64, pub frames_submitted: u64,
@@ -1,5 +1,5 @@
/* /*
* lumen-core C ABI harness M1 acceptance. * punktfunk-core C ABI harness M1 acceptance.
* *
* Proves the core links from C and round-trips encoded access units through the full * 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 * 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`). * Build/run: see tests/c/run.sh (also driven by `cargo test --test c_abi`).
*/ */
#include "lumen_core.h" #include "punktfunk_core.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
static LumenConfig make_config(uint32_t role, uint32_t drop_period) { static PunktfunkConfig make_config(uint32_t role, uint32_t drop_period) {
LumenConfig c; PunktfunkConfig c;
memset(&c, 0, sizeof(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.role = role; /* 0 = host, 1 = client */
c.phase = 1; /* P1, GameStream-compatible */ c.phase = 1; /* P1, GameStream-compatible */
c.fec_scheme = 0; /* GF(2^8) */ 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) { 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 */ const uint32_t DROP_PERIOD = 8; /* drop 1 of every 8 packets */
LumenConfig host_cfg = make_config(0, DROP_PERIOD); PunktfunkConfig host_cfg = make_config(0, DROP_PERIOD);
LumenConfig client_cfg = make_config(1, DROP_PERIOD); PunktfunkConfig client_cfg = make_config(1, DROP_PERIOD);
LumenSession *host = NULL; PunktfunkSession *host = NULL;
LumenSession *client = NULL; PunktfunkSession *client = NULL;
LumenStatus rc = lumen_test_loopback_pair(&host_cfg, &client_cfg, &host, &client); PunktfunkStatus rc = punktfunk_test_loopback_pair(&host_cfg, &client_cfg, &host, &client);
if (rc != LUMEN_STATUS_OK || !host || !client) { if (rc != PUNKTFUNK_STATUS_OK || !host || !client) {
fprintf(stderr, "FAIL: loopback_pair rc=%d\n", (int)rc); fprintf(stderr, "FAIL: loopback_pair rc=%d\n", (int)rc);
return 1; return 1;
} }
@@ -55,17 +55,17 @@ int main(void) {
buf[i] = (uint8_t)((i * 131u) + (unsigned)f * 17u); buf[i] = (uint8_t)((i * 131u) + (unsigned)f * 17u);
} }
rc = lumen_host_submit_frame(host, buf, FRAME_LEN, (uint64_t)f * 1000000u, 0); rc = punktfunk_host_submit_frame(host, buf, FRAME_LEN, (uint64_t)f * 1000000u, 0);
if (rc != LUMEN_STATUS_OK) { if (rc != PUNKTFUNK_STATUS_OK) {
fprintf(stderr, "FAIL: submit frame %d rc=%d\n", f, (int)rc); fprintf(stderr, "FAIL: submit frame %d rc=%d\n", f, (int)rc);
failures++; failures++;
continue; continue;
} }
LumenFrame out; PunktfunkFrame out;
memset(&out, 0, sizeof(out)); memset(&out, 0, sizeof(out));
rc = lumen_client_poll_frame(client, &out); rc = punktfunk_client_poll_frame(client, &out);
if (rc != LUMEN_STATUS_OK) { if (rc != PUNKTFUNK_STATUS_OK) {
fprintf(stderr, "FAIL: poll frame %d rc=%d (expected recovery)\n", f, (int)rc); fprintf(stderr, "FAIL: poll frame %d rc=%d (expected recovery)\n", f, (int)rc);
failures++; failures++;
continue; continue;
@@ -82,9 +82,9 @@ int main(void) {
} }
} }
LumenStats st; PunktfunkStats st;
memset(&st, 0, sizeof(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", 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.frames_completed,
(unsigned long long)st.fec_recovered_shards, (unsigned long long)st.fec_recovered_shards,
@@ -97,8 +97,8 @@ int main(void) {
} }
free(buf); free(buf);
lumen_session_free(host); punktfunk_session_free(host);
lumen_session_free(client); punktfunk_session_free(client);
if (failures == 0) { if (failures == 0) {
printf("PASS: %d frames round-tripped byte-exact through lossy loopback\n", FRAMES); printf("PASS: %d frames round-tripped byte-exact through lossy loopback\n", FRAMES);
@@ -1,30 +1,30 @@
#!/usr/bin/env bash #!/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). # Proves the core links from C. Works on Linux and macOS (link flags come from rustc).
set -euo pipefail set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 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" cd "$ws"
profile="${1:-debug}" profile="${1:-debug}"
build_flag="" build_flag=""
[ "$profile" = "release" ] && build_flag="--release" [ "$profile" = "release" ] && build_flag="--release"
echo ">> building lumen-core staticlib ($profile)" echo ">> building punktfunk-core staticlib ($profile)"
cargo build -p lumen-core $build_flag >/dev/null 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" header_dir="$ws/include"
[ -f "$staticlib" ] || { echo "missing $staticlib"; exit 1; } [ -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. # 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)" --print native-static-libs 2>&1 | sed -n 's/.*native-static-libs: //p' | tail -1)"
echo ">> native libs: ${native_libs:-<none>}" echo ">> native libs: ${native_libs:-<none>}"
out="$(mktemp -d)/lumen_harness" out="$(mktemp -d)/punktfunk_harness"
cc="${CC:-cc}" cc="${CC:-cc}"
echo ">> compiling + linking harness" echo ">> compiling + linking harness"
$cc -std=c11 -Wall -Wextra -O2 -I "$header_dir" \ $cc -std=c11 -Wall -Wextra -O2 -I "$header_dir" \
@@ -1,5 +1,5 @@
//! Runs the C ABI harness under `cargo test`: compiles `tests/c/harness.c`, links it //! 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 //! 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 //! link flags) is `tests/c/run.sh`; this mirrors it so `cargo test` alone covers the
//! C boundary. //! C boundary.
@@ -21,13 +21,13 @@ fn native_libs() -> &'static [&'static str] {
} }
fn ensure_staticlib(profile_dir: &Path) -> PathBuf { 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() { if !staticlib.exists() {
// `cargo test` doesn't always emit the standalone staticlib; build it. The // `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. // 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 cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
let _ = Command::new(cargo) let _ = Command::new(cargo)
.args(["build", "-p", "lumen-core"]) .args(["build", "-p", "punktfunk-core"])
.status(); .status();
} }
staticlib staticlib
@@ -35,7 +35,7 @@ fn ensure_staticlib(profile_dir: &Path) -> PathBuf {
#[test] #[test]
fn c_abi_harness_round_trips() { 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 harness = manifest.join("tests/c/harness.c");
let include = manifest.join("../../include"); let include = manifest.join("../../include");
@@ -50,16 +50,16 @@ fn c_abi_harness_round_trips() {
let staticlib = ensure_staticlib(&profile_dir); let staticlib = ensure_staticlib(&profile_dir);
assert!( assert!(
staticlib.exists(), staticlib.exists(),
"staticlib not found at {} (run `cargo build -p lumen-core`)", "staticlib not found at {} (run `cargo build -p punktfunk-core`)",
staticlib.display() staticlib.display()
); );
assert!( assert!(
include.join("lumen_core.h").exists(), include.join("punktfunk_core.h").exists(),
"generated header missing; build lumen-core to regenerate it" "generated header missing; build punktfunk-core to regenerate it"
); );
let cc = std::env::var("CC").unwrap_or_else(|_| "cc".into()); 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); let mut compile = Command::new(&cc);
compile compile
@@ -3,19 +3,19 @@
//! byte-exact recovery, for both FEC schemes, with and without encryption. Plus //! byte-exact recovery, for both FEC schemes, with and without encryption. Plus
//! property tests over the FEC layer's loss patterns. //! 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 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 { fn config(role: Role, scheme: FecScheme, encrypt: bool, drop_period: u32) -> Config {
Config { Config {
role, role,
phase: match scheme { phase: match scheme {
FecScheme::Gf8 => ProtocolPhase::P1GameStream, FecScheme::Gf8 => ProtocolPhase::P1GameStream,
FecScheme::Gf16 => ProtocolPhase::P2Lumen, FecScheme::Gf16 => ProtocolPhase::P2Punktfunk,
}, },
fec: FecConfig { fec: FecConfig {
scheme, scheme,
@@ -38,7 +38,7 @@ fn run_stream(
encrypt: bool, encrypt: bool,
drop_period: u32, drop_period: u32,
frames: &[Vec<u8>], frames: &[Vec<u8>],
) -> lumen_core::Stats { ) -> punktfunk_core::Stats {
let (host_tp, client_tp) = loopback_pair(drop_period, 0); let (host_tp, client_tp) = loopback_pair(drop_period, 0);
let mut host = Session::new( let mut host = Session::new(
config(Role::Host, scheme, encrypt, drop_period), config(Role::Host, scheme, encrypt, drop_period),
@@ -1,6 +1,6 @@
[package] [package]
name = "lumen-host" name = "punktfunk-host"
description = "lumen Linux streaming host: virtual display, capture, encode, input injection" description = "punktfunk Linux streaming host: virtual display, capture, encode, input injection"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@@ -9,8 +9,8 @@ authors.workspace = true
repository.workspace = true repository.workspace = true
[dependencies] [dependencies]
lumen-core = { path = "../lumen-core", features = ["quic"] } punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
# M3 native control plane (the `lumen/1` QUIC handshake; data plane stays native-thread UDP). # M3 native control plane (the `punktfunk/1` QUIC handshake; data plane stays native-thread UDP).
quinn = "0.11" quinn = "0.11"
anyhow = "1" anyhow = "1"
tracing = "0.1" tracing = "0.1"
@@ -20,7 +20,7 @@ impl PwAudioCapturer {
pub fn open() -> Result<PwAudioCapturer> { pub fn open() -> Result<PwAudioCapturer> {
let (tx, rx) = sync_channel::<Vec<f32>>(64); let (tx, rx) = sync_channel::<Vec<f32>>(64);
thread::Builder::new() thread::Builder::new()
.name("lumen-pw-audio".into()) .name("punktfunk-pw-audio".into())
.spawn(move || { .spawn(move || {
if let Err(e) = pw_thread(tx) { if let Err(e) = pw_thread(tx) {
tracing::error!(error = %format!("{e:#}"), "pipewire audio thread failed"); tracing::error!(error = %format!("{e:#}"), "pipewire audio thread failed");
@@ -60,7 +60,7 @@ fn pw_thread(tx: std::sync::mpsc::SyncSender<Vec<f32>>) -> Result<()> {
let stream = pw::stream::StreamBox::new( let stream = pw::stream::StreamBox::new(
&core, &core,
"lumen-audio", "punktfunk-audio",
properties! { properties! {
*pw::keys::MEDIA_TYPE => "Audio", *pw::keys::MEDIA_TYPE => "Audio",
*pw::keys::MEDIA_CATEGORY => "Capture", *pw::keys::MEDIA_CATEGORY => "Capture",
@@ -90,7 +90,7 @@ pub trait Capturer: Send {
} }
/// A deterministic moving test pattern (BGRx). Lets M0 exercise the encode → file → /// 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. /// content (a sweeping bar + animated gradient) so the encoded output is verifiable.
pub struct SyntheticCapturer { pub struct SyntheticCapturer {
width: u32, width: u32,
@@ -45,7 +45,7 @@ impl PortalCapturer {
// Portal handshake (async) on its own thread; hands back the PW fd + node id. // Portal handshake (async) on its own thread; hands back the PW fd + node id.
let (setup_tx, setup_rx) = std::sync::mpsc::channel::<Result<(OwnedFd, u32), String>>(); let (setup_tx, setup_rx) = std::sync::mpsc::channel::<Result<(OwnedFd, u32), String>>();
thread::Builder::new() thread::Builder::new()
.name("lumen-portal".into()) .name("punktfunk-portal".into())
.spawn(move || { .spawn(move || {
if anchored { if anchored {
portal_thread_remote_desktop(setup_tx) portal_thread_remote_desktop(setup_tx)
@@ -105,7 +105,7 @@ fn spawn_pipewire(
let active_cb = active.clone(); let active_cb = active.clone();
let zerocopy = crate::zerocopy::enabled(); let zerocopy = crate::zerocopy::enabled();
thread::Builder::new() thread::Builder::new()
.name("lumen-pipewire".into()) .name("punktfunk-pipewire".into())
.spawn(move || { .spawn(move || {
if let Err(e) = if let Err(e) =
pipewire::pipewire_thread(fd, node_id, frame_tx, active_cb, zerocopy, preferred) pipewire::pipewire_thread(fd, node_id, frame_tx, active_cb, zerocopy, preferred)
@@ -652,7 +652,7 @@ mod pipewire {
let stream = pw::stream::StreamBox::new( let stream = pw::stream::StreamBox::new(
&core, &core,
"lumen-screencast", "punktfunk-screencast",
properties! { properties! {
*pw::keys::MEDIA_TYPE => "Video", *pw::keys::MEDIA_TYPE => "Video",
*pw::keys::MEDIA_CATEGORY => "Capture", *pw::keys::MEDIA_CATEGORY => "Capture",
@@ -871,9 +871,9 @@ mod pipewire {
.register() .register()
.context("register stream listener")?; .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). // 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() .ok()
.and_then(|v| v.split_once('x').map(|(w, h)| (w.parse(), h.parse()))) .and_then(|v| v.split_once('x').map(|(w, h)| (w.parse(), h.parse())))
.and_then(|(w, h)| Some((w.ok()?, h.ok()?))); .and_then(|(w, h)| Some((w.ok()?, h.ok()?)));
@@ -6,7 +6,7 @@
use crate::capture::{CapturedFrame, PixelFormat}; use crate::capture::{CapturedFrame, PixelFormat};
use anyhow::Result; 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 /// `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 /// keyframe carries its own VPS/SPS/PPS — the bytes are both a playable elementary
/// stream and a self-contained AU for the wire. /// stream and a self-contained AU for the wire.
@@ -139,7 +139,7 @@ impl NvencEncoder {
cuda: bool, cuda: bool,
) -> Result<Self> { ) -> Result<Self> {
ffmpeg::init().context("ffmpeg init")?; 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 unsafe { ffi::av_log_set_level(48) }; // AV_LOG_DEBUG — surface NVENC hw-frame rejects
} }
let name = codec.nvenc_name(); 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, // 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 // @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). // 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 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() { match split.as_deref() {
Some(mode) => opts.set("split_encode_mode", mode), Some(mode) => opts.set("split_encode_mode", mode),
None if matches!(codec, Codec::H265 | Codec::Av1) && pix_rate > 1_000_000_000 => { None if matches!(codec, Codec::H265 | Codec::Av1) && pix_rate > 1_000_000_000 => {
@@ -1,6 +1,6 @@
//! The app catalog: what `/applist` advertises and what `/launch?appid=N` selects. Each entry //! 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 //! 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 //! ```json
//! [ {"id":1,"title":"Desktop"}, //! [ {"id":1,"title":"Desktop"},
@@ -20,7 +20,7 @@ pub struct AppEntry {
} }
fn config_path() -> Option<std::path::PathBuf> { fn config_path() -> Option<std::path::PathBuf> {
Some(std::path::Path::new(&std::env::var("HOME").ok()?).join(".config/lumen/apps.json")) Some(std::path::Path::new(&std::env::var("HOME").ok()?).join(".config/punktfunk/apps.json"))
} }
fn parse_compositor(s: &str) -> Option<crate::vdisplay::Compositor> { fn parse_compositor(s: &str) -> Option<crate::vdisplay::Compositor> {
@@ -38,7 +38,7 @@ pub type AudioCapSlot = Arc<std::sync::Mutex<Option<Box<dyn AudioCapturer>>>>;
/// `gcm_key`/`rikeyid` come from `/launch` and key the AES-CBC payload encryption. /// `gcm_key`/`rikeyid` come from `/launch` and key the AES-CBC payload encryption.
pub fn start(running: Arc<AtomicBool>, gcm_key: [u8; 16], rikeyid: i32, audio_cap: AudioCapSlot) { pub fn start(running: Arc<AtomicBool>, gcm_key: [u8; 16], rikeyid: i32, audio_cap: AudioCapSlot) {
let _ = std::thread::Builder::new() let _ = std::thread::Builder::new()
.name("lumen-audio".into()) .name("punktfunk-audio".into())
.spawn(move || { .spawn(move || {
tracing::info!("audio stream starting"); tracing::info!("audio stream starting");
if let Err(e) = run(&running, &gcm_key, rikeyid, &audio_cap) { 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. // each frame at its 5 ms slot instead. Production is real-time, so the backlog stays small.
let start = Instant::now(); let start = Instant::now();
let mut frame_no: u64 = 0; let mut frame_no: u64 = 0;
// Optional linear gain for quiet capture sources (LUMEN_AUDIO_GAIN, default 1.0). // Optional linear gain for quiet capture sources (PUNKTFUNK_AUDIO_GAIN, default 1.0).
let gain: f32 = std::env::var("LUMEN_AUDIO_GAIN") let gain: f32 = std::env::var("PUNKTFUNK_AUDIO_GAIN")
.ok() .ok()
.and_then(|v| v.parse().ok()) .and_then(|v| v.parse().ok())
.unwrap_or(1.0); .unwrap_or(1.0);
@@ -38,7 +38,7 @@ impl ServerIdentity {
.with_context(|| format!("write {}", cert_path.display()))?; .with_context(|| format!("write {}", cert_path.display()))?;
fs::write(&key_path, &k) fs::write(&key_path, &k)
.with_context(|| format!("write {}", key_path.display()))?; .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) (c, k)
} }
}; };
@@ -70,7 +70,7 @@ fn generate() -> Result<(String, String)> {
let mut params = rcgen::CertificateParams::new(Vec::<String>::new()).context("cert params")?; let mut params = rcgen::CertificateParams::new(Vec::<String>::new()).context("cert params")?;
params params
.distinguished_name .distinguished_name
.push(rcgen::DnType::CommonName, "lumen"); .push(rcgen::DnType::CommonName, "punktfunk");
params.not_before = rcgen::date_time_ymd(2020, 1, 1); params.not_before = rcgen::date_time_ymd(2020, 1, 1);
params.not_after = rcgen::date_time_ymd(2040, 1, 1); params.not_after = rcgen::date_time_ymd(2040, 1, 1);
let cert = params.self_signed(&key).context("self-sign cert")?; let cert = params.self_signed(&key).context("self-sign cert")?;
@@ -51,7 +51,7 @@ pub fn spawn(state: Arc<AppState>) -> Result<()> {
tracing::info!(port = CONTROL_PORT, "ENet control listening"); tracing::info!(port = CONTROL_PORT, "ENet control listening");
std::thread::Builder::new() std::thread::Builder::new()
.name("lumen-control".into()) .name("punktfunk-control".into())
.spawn(move || { .spawn(move || {
// Thread-local (the injector owns non-Send Wayland/xkb state, so it must be // 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). // 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. // 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 // Backend auto-selects per desktop (wlr on Sway, libei on KWin/GNOME); override with
// LUMEN_INPUT_BACKEND. // PUNKTFUNK_INPUT_BACKEND.
if injector.is_none() { if injector.is_none() {
let backend = crate::inject::default_backend(); let backend = crate::inject::default_backend();
match crate::inject::open(backend) { match crate::inject::open(backend) {
@@ -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**, //! 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 //! SHA-256 (host appversion major ≥ 7), and RSA-PKCS1v15-SHA256 signatures. See the
//! `serverinfo + pairing` section of `docs/research/gamestream-protocol-research.json`. //! `serverinfo + pairing` section of `docs/research/gamestream-protocol-research.json`.
@@ -1,6 +1,6 @@
//! Decode the GameStream input wire format (carried AES-GCM-encrypted on the ENet control //! Decode the GameStream input wire format (carried AES-GCM-encrypted on the ENet control
//! stream — see [`super::control`]) into platform-agnostic //! 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 //! 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, //! 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` //! 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). //! (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]`). /// Inner control-message type for input (moonlight `packetTypesGen7[IDX_INPUT_DATA]`).
const INPUT_DATA_TYPE: u16 = 0x0206; const INPUT_DATA_TYPE: u16 = 0x0206;
@@ -1,7 +1,7 @@
//! GameStream (P1) control plane — what a stock Moonlight/Artemis client talks to around //! 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, //! 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 //! 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 //! Status: P1.1 — mDNS `_nvstream._tcp` advertisement + `/serverinfo`. Pairing, RTSP, and
//! the media streams follow (see the M2 task list / plan). //! the media streams follow (see the M2 task list / plan).
@@ -154,7 +154,7 @@ pub fn serve(mgmt: crate::mgmt::Options) -> Result<()> {
hostname = %state.host.hostname, hostname = %state.host.hostname,
uniqueid = %state.host.uniqueid, uniqueid = %state.host.uniqueid,
ip = %state.host.local_ip, 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")?; let rt = tokio::runtime::Runtime::new().context("build tokio runtime")?;
rt.block_on(async move { 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 { fn config_dir() -> PathBuf {
let base = std::env::var_os("XDG_CONFIG_HOME") let base = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from) .map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config"))) .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))
.unwrap_or_else(|| PathBuf::from(".")); .unwrap_or_else(|| PathBuf::from("."));
base.join("lumen") base.join("punktfunk")
} }
fn hostname_string() -> String { fn hostname_string() -> String {
@@ -182,7 +182,7 @@ fn hostname_string() -> String {
.ok() .ok()
.map(|s| s.trim().to_string()) .map(|s| s.trim().to_string())
.filter(|s| !s.is_empty()) .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. /// Load the persisted host uniqueid, or mint one (from the kernel UUID source) and store it.
@@ -212,7 +212,7 @@ fn primary_local_ip() -> Option<IpAddr> {
/// Where the paired-client allow-list persists (survives host restarts, like Sunshine). /// Where the paired-client allow-list persists (survives host restarts, like Sunshine).
fn paired_path() -> Option<std::path::PathBuf> { fn paired_path() -> Option<std::path::PathBuf> {
Some(std::path::Path::new(&std::env::var("HOME").ok()?).join(".config/lumen/paired.json")) Some(std::path::Path::new(&std::env::var("HOME").ok()?).join(".config/punktfunk/paired.json"))
} }
/// Load the persisted paired-client certificate DERs (empty on first run / parse failure). /// Load the persisted paired-client certificate DERs (empty on first run / parse failure).
@@ -1,5 +1,5 @@
//! The nvhttp servers: plain HTTP on 47989 and mutual-TLS on 47984. Serves `/serverinfo`, //! 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 //! `/pin` endpoint to deliver the Moonlight-displayed PIN. Over HTTPS the client is
//! mutual-TLS-authenticated, so `/serverinfo` reports `PairStatus=1` there. //! mutual-TLS-authenticated, so `/serverinfo` reports `PairStatus=1` there.
@@ -27,7 +27,7 @@ pub fn spawn(state: Arc<AppState>) -> Result<()> {
.with_context(|| format!("bind RTSP {RTSP_PORT}"))?; .with_context(|| format!("bind RTSP {RTSP_PORT}"))?;
tracing::info!(port = RTSP_PORT, "RTSP listening"); tracing::info!(port = RTSP_PORT, "RTSP listening");
std::thread::Builder::new() std::thread::Builder::new()
.name("lumen-rtsp".into()) .name("punktfunk-rtsp".into())
.spawn(move || { .spawn(move || {
for conn in listener.incoming() { for conn in listener.incoming() {
match conn { match conn {
@@ -1,6 +1,6 @@
//! The video data plane: on RTSP PLAY, learn the client's UDP endpoint (it pings the video //! 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 //! 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. //! a synthetic test pattern (default). Runs on its own native thread.
use super::video::{FrameType, VideoPacketizer}; use super::video::{FrameType, VideoPacketizer};
@@ -42,7 +42,7 @@ pub fn start(
video_cap: CapturerSlot, video_cap: CapturerSlot,
) { ) {
let _ = std::thread::Builder::new() let _ = std::thread::Builder::new()
.name("lumen-video".into()) .name("punktfunk-video".into())
.spawn(move || { .spawn(move || {
tracing::info!(?cfg, "video stream starting"); tracing::info!(?cfg, "video stream starting");
if let Err(e) = run(cfg, app.as_ref(), &running, &force_idr, &video_cap) { 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 // 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 // `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). // 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 // The launched app picks the compositor (e.g. gamescope for game entries) and the
// nested command; env vars remain manual overrides / fallbacks. // nested command; env vars remain manual overrides / fallbacks.
let compositor = app let compositor = app
@@ -93,7 +93,7 @@ fn run(
if let Some(cmd) = app.and_then(|a| a.cmd.as_deref()) { 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 // The gamescope backend reads the nested command from this env var; setting it
// per-launch is safe (one stream session at a time). // 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!( tracing::info!(
?compositor, ?compositor,
@@ -104,7 +104,7 @@ fn run(
); );
let mut vd = crate::vdisplay::open(compositor).context("open virtual display")?; let mut vd = crate::vdisplay::open(compositor).context("open virtual display")?;
let vout = vd let vout = vd
.create(lumen_core::Mode { .create(punktfunk_core::Mode {
width: cfg.width, width: cfg.width,
height: cfg.height, height: cfg.height,
refresh_hz: cfg.fps, refresh_hz: cfg.fps,
@@ -123,7 +123,7 @@ fn run(
tracing::info!("video source: reusing capturer"); tracing::info!("video source: reusing capturer");
c 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"); tracing::info!("video source: portal desktop capture");
capture::open_portal_monitor().context("open portal capturer")? capture::open_portal_monitor().context("open portal capturer")?
} }
@@ -202,7 +202,7 @@ fn spawn_sender(
drop_pct: u32, drop_pct: u32,
) -> Result<()> { ) -> Result<()> {
std::thread::Builder::new() std::thread::Builder::new()
.name("lumen-send".into()) .name("punktfunk-send".into())
.spawn(move || { .spawn(move || {
// Chunk pacing: 16 packets per burst, bursts spread across the send budget. // Chunk pacing: 16 packets per burst, bursts spread across the send budget.
const PACE_CHUNK: usize = 16; const PACE_CHUNK: usize = 16;
@@ -276,8 +276,8 @@ fn stream_body(
frame.is_cuda(), frame.is_cuda(),
) )
.context("open NVENC for stream")?; .context("open NVENC for stream")?;
// FEC overhead percent (Sunshine default 20). Override with LUMEN_FEC_PCT (0 = data-only). // FEC overhead percent (Sunshine default 20). Override with PUNKTFUNK_FEC_PCT (0 = data-only).
let fec_pct: u8 = std::env::var("LUMEN_FEC_PCT") let fec_pct: u8 = std::env::var("PUNKTFUNK_FEC_PCT")
.ok() .ok()
.and_then(|v| v.parse().ok()) .and_then(|v| v.parse().ok())
.unwrap_or(20); .unwrap_or(20);
@@ -294,7 +294,7 @@ fn stream_body(
let mut fps_t = Instant::now(); let mut fps_t = Instant::now();
let stream_start = Instant::now(); let stream_start = Instant::now();
// Test knob: drop this % of outbound packets to exercise FEC recovery (0 = off). // 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() .ok()
.and_then(|v| v.parse().ok()) .and_then(|v| v.parse().ok())
.unwrap_or(0); .unwrap_or(0);
@@ -313,9 +313,9 @@ fn stream_body(
drop_pct, 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). // 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) = let (mut mx_cap, mut mx_enc, mut mx_pkt, mut mx_send, mut mx_pkts, mut uniq) =
(0u128, 0u128, 0u128, 0u128, 0usize, 0u32); (0u128, 0u128, 0u128, 0u128, 0usize, 0u32);
// Absolute next-frame deadline — the single pacing clock for the loop. // Absolute next-frame deadline — the single pacing clock for the loop.
@@ -6,7 +6,7 @@
//! `docs/research/gamestream-protocol-research.json` (video plane). //! `docs/research/gamestream-protocol-research.json` (video plane).
//! //!
//! FEC (P1.5): each block carries `m = ⌈k·pct/100⌉` ReedSolomon parity shards generated by //! FEC (P1.5): each block carries `m = ⌈k·pct/100⌉` ReedSolomon 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 //! over the **whole `blocksize` shard** — Moonlight decodes over `packetSize + 16` bytes from
//! the datagram start (`RtpVideoQueue.c`), and rejects a recovered shard whose reconstructed //! 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, //! `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 //! Sunshine `stream.cpp`. `pct = 0` falls back to data-shards-only. Plaintext (AES-GCM video
//! encryption is negotiated off for now). //! 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. /// RTP `header` byte: version 2 (0x80) | extension (0x10) — Moonlight keys on the extension.
const RTP_HEADER_BYTE: u8 = 0x80 | 0x10; const RTP_HEADER_BYTE: u8 = 0x80 | 0x10;
@@ -1,4 +1,4 @@
//! Input injection (plan §4): turn client [`lumen_core::input::InputEvent`]s into host input. //! Input injection (plan §4): turn client [`punktfunk_core::input::InputEvent`]s into host input.
//! //!
//! The headless Sway compositor runs with `WLR_LIBINPUT_NO_DEVICES=1`, so kernel `uinput` //! 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 //! devices are never picked up. Instead we inject through the wlroots virtual-input Wayland
@@ -10,7 +10,7 @@
//! keysyms correctly. //! keysyms correctly.
use anyhow::Result; 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 /// 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 /// resources (a Wayland connection, an xkb state) and lives entirely on the control thread
@@ -77,9 +77,9 @@ pub fn open(backend: Backend) -> Result<Box<dyn InputInjector>> {
/// portal), so a gamescope session injects directly into it. wlroots/Sway only implements the /// 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 /// 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. /// 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 { 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() { match v.trim().to_ascii_lowercase().as_str() {
"wlr" | "wlroots" | "wlrvirtual" => return Backend::WlrVirtual, "wlr" | "wlroots" | "wlrvirtual" => return Backend::WlrVirtual,
"libei" | "ei" | "portal" => return Backend::Libei, "libei" | "ei" | "portal" => return Backend::Libei,
@@ -87,11 +87,13 @@ pub fn default_backend() -> Backend {
"uinput" => return Backend::Uinput, "uinput" => return Backend::Uinput,
other => tracing::warn!( other => tracing::warn!(
value = other, 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; return Backend::GamescopeEi;
} }
let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default(); let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
@@ -13,7 +13,7 @@
//! //!
//! All ioctl numbers/struct layouts below were verified against this generation's //! All ioctl numbers/struct layouts below were verified against this generation's
//! `<linux/uinput.h>` on x86_64. `/dev/uinput` needs a udev rule + `input` group membership //! `<linux/uinput.h>` on x86_64. `/dev/uinput` needs a udev rule + `input` group membership
//! (see `scripts/60-lumen.rules`); creation fails with a clear error otherwise. //! (see `scripts/60-punktfunk.rules`); creation fails with a clear error otherwise.
use crate::gamestream::gamepad::{self, GamepadFrame, MAX_PADS}; use crate::gamestream::gamepad::{self, GamepadFrame, MAX_PADS};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
@@ -213,7 +213,7 @@ impl VirtualPad {
if raw < 0 { if raw < 0 {
bail!( bail!(
"open /dev/uinput: {} (install the udev rule granting the 'input' group access \ "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() std::io::Error::last_os_error()
); );
} }
@@ -28,7 +28,7 @@ use ashpd::desktop::{
CreateSessionOptions, PersistMode, CreateSessionOptions, PersistMode,
}; };
use futures_util::StreamExt; use futures_util::StreamExt;
use lumen_core::input::{InputEvent, InputKind}; use punktfunk_core::input::{InputEvent, InputKind};
use reis::ei; use reis::ei;
use reis::event::{DeviceCapability, EiEvent}; use reis::event::{DeviceCapability, EiEvent};
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
@@ -61,7 +61,7 @@ impl LibeiInjector {
pub fn open_with(source: EiSource) -> Result<Self> { pub fn open_with(source: EiSource) -> Result<Self> {
let (tx, rx) = unbounded_channel::<InputEvent>(); let (tx, rx) = unbounded_channel::<InputEvent>();
std::thread::Builder::new() std::thread::Builder::new()
.name("lumen-libei".into()) .name("punktfunk-libei".into())
.spawn(move || worker(rx, source)) .spawn(move || worker(rx, source))
.map_err(|e| anyhow!("spawn libei worker thread: {e}"))?; .map_err(|e| anyhow!("spawn libei worker thread: {e}"))?;
// Return immediately — the portal/socket handshake must NOT run on the caller's // Return immediately — the portal/socket handshake must NOT run on the caller's
@@ -156,7 +156,7 @@ async fn connect(source: EiSource) -> Result<Connected> {
}; };
let context = ei::Context::new(stream).map_err(|e| anyhow!("reis EI context: {e}"))?; let context = ei::Context::new(stream).map_err(|e| anyhow!("reis EI context: {e}"))?;
let (_conn, events) = context let (_conn, events) = context
.handshake_tokio("lumen-host", ei::handshake::ContextType::Sender) .handshake_tokio("punktfunk-host", ei::handshake::ContextType::Sender)
.await .await
.map_err(|e| anyhow!("EI handshake: {e}"))?; .map_err(|e| anyhow!("EI handshake: {e}"))?;
Ok((portal, context, events)) Ok((portal, context, events))
@@ -7,7 +7,7 @@
use super::{gs_button_to_evdev, vk_to_evdev, InputEvent, InputInjector}; use super::{gs_button_to_evdev, vk_to_evdev, InputEvent, InputInjector};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use lumen_core::input::InputKind; use punktfunk_core::input::InputKind;
use std::io::Write; use std::io::Write;
use std::os::fd::{AsFd, FromRawFd}; use std::os::fd::{AsFd, FromRawFd};
use std::time::Instant; 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). /// Create an anonymous in-memory file holding `s` + a trailing NUL (for the keymap fd).
fn memfd_with(s: &str) -> Result<std::fs::File> { fn memfd_with(s: &str) -> Result<std::fs::File> {
let name = b"lumen-keymap\0"; let name = b"punktfunk-keymap\0";
let fd = unsafe { libc::memfd_create(name.as_ptr() as *const libc::c_char, libc::MFD_CLOEXEC) }; let fd = unsafe { libc::memfd_create(name.as_ptr() as *const libc::c_char, libc::MFD_CLOEXEC) };
if fd < 0 { if fd < 0 {
bail!("memfd_create failed: {}", std::io::Error::last_os_error()); bail!("memfd_create failed: {}", std::io::Error::last_os_error());
@@ -1,5 +1,5 @@
//! M0 — the pipeline spike (plan §8): capture → NVENC encode → playable file, with the //! 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 //! in-process loopback to prove the core's FEC + packetize + reassemble path on real
//! encoder output. //! encoder output.
//! //!
@@ -11,8 +11,8 @@
use crate::capture::{self, Capturer, SyntheticCapturer}; use crate::capture::{self, Capturer, SyntheticCapturer};
use crate::encode::{self, Codec, EncodedFrame, Encoder}; use crate::encode::{self, Codec, EncodedFrame, Encoder};
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use lumen_core::packet::{FLAG_PIC, FLAG_SOF}; use punktfunk_core::packet::{FLAG_PIC, FLAG_SOF};
use lumen_core::{Config, Role, Session}; use punktfunk_core::{Config, Role, Session};
use std::fs::File; use std::fs::File;
use std::io::{BufWriter, Write}; use std::io::{BufWriter, Write};
use std::path::PathBuf; use std::path::PathBuf;
@@ -41,7 +41,7 @@ pub struct Options {
pub bitrate_bps: u64, pub bitrate_bps: u64,
/// Raw Annex-B elementary-stream sink (`.h265`/`.h264`/`.ivf-less .obu`); playable. /// Raw Annex-B elementary-stream sink (`.h265`/`.h264`/`.ivf-less .obu`); playable.
pub out: PathBuf, 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, pub loopback: bool,
} }
@@ -66,11 +66,11 @@ pub fn run(opts: Options) -> Result<()> {
width = opts.width, width = opts.width,
height = opts.height, height = opts.height,
?compositor, ?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 mut vd = crate::vdisplay::open(compositor).context("open virtual display")?;
let vout = vd let vout = vd
.create(lumen_core::Mode { .create(punktfunk_core::Mode {
width: opts.width, width: opts.width,
height: opts.height, height: opts.height,
refresh_hz: opts.fps, refresh_hz: opts.fps,
@@ -112,7 +112,7 @@ pub fn run(opts: Options) -> Result<()> {
); );
let mut lb = if opts.loopback { let mut lb = if opts.loopback {
Some(Loopback::new().context("build lumen-core loopback")?) Some(Loopback::new().context("build punktfunk-core loopback")?)
} else { } else {
None None
}; };
@@ -153,7 +153,7 @@ pub fn run(opts: Options) -> Result<()> {
lb.report(); lb.report();
if lb.mismatches > 0 || lb.recovered != lb.submitted { if lb.mismatches > 0 || lb.recovered != lb.submitted {
return Err(anyhow!( return Err(anyhow!(
"lumen-core loopback verification FAILED: {} mismatches, {}/{} AUs recovered", "punktfunk-core loopback verification FAILED: {} mismatches, {}/{} AUs recovered",
lb.mismatches, lb.mismatches,
lb.recovered, lb.recovered,
lb.submitted lb.submitted
@@ -191,7 +191,7 @@ fn drain_encoder(
Ok(()) 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 /// FEC-protected, packetized, sent, then reassembled on the client and byte-compared to the
/// original — exercising the core on real encoder output (the M0 "feed into a Session" goal). /// original — exercising the core on real encoder output (the M0 "feed into a Session" goal).
struct Loopback { struct Loopback {
@@ -205,7 +205,7 @@ struct Loopback {
impl Loopback { impl Loopback {
fn new() -> Result<Loopback> { fn new() -> Result<Loopback> {
let (host_tx, client_tx) = lumen_core::transport::loopback_pair(0, 0); let (host_tx, client_tx) = punktfunk_core::transport::loopback_pair(0, 0);
let host = Session::new(Config::p1_defaults(Role::Host), Box::new(host_tx)) let host = Session::new(Config::p1_defaults(Role::Host), Box::new(host_tx))
.map_err(|e| anyhow!("host session: {e:?}"))?; .map_err(|e| anyhow!("host session: {e:?}"))?;
let client = Session::new(Config::p1_defaults(Role::Client), Box::new(client_tx)) 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:?}")), Err(e) => return Err(anyhow!("client poll_frame: {e:?}")),
} }
} }
@@ -259,7 +259,7 @@ impl Loopback {
recovered = self.recovered, recovered = self.recovered,
mismatches = self.mismatches, mismatches = self.mismatches,
bytes = self.bytes, bytes = self.bytes,
"lumen-core loopback: AUs FEC-packetized → sent → reassembled & verified" "punktfunk-core loopback: AUs FEC-packetized → sent → reassembled & verified"
); );
} }
} }
@@ -1,5 +1,5 @@
//! M3 — the `lumen/1` native host: QUIC control plane + the hardened M1 data plane over UDP. //! M3 — the `punktfunk/1` native host: QUIC control plane + the hardened M1 data plane over UDP.
//! This is lumen's own protocol, past the GameStream compatibility layer: //! This is punktfunk's own protocol, past the GameStream compatibility layer:
//! //!
//! * the Welcome negotiates **GF(2¹⁶) Leopard FEC** (inexpressible in GameStream) + AES-GCM; //! * 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 //! * 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 //! * video frames carry a wall-clock `pts_ns`, so a same-host client measures the full
//! capture→encode→FEC→UDP→reassemble latency per frame. //! 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 //! [--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). //! 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 → //! 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; //! 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. //! with GameStream pairing) and logs the SHA-256 fingerprint clients pin.
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use lumen_core::config::{FecConfig, FecScheme, Role}; use punktfunk_core::config::{FecConfig, FecScheme, Role};
use lumen_core::input::{InputEvent, InputKind}; use punktfunk_core::input::{InputEvent, InputKind};
use lumen_core::packet::{FLAG_PIC, FLAG_SOF}; use punktfunk_core::packet::{FLAG_PIC, FLAG_SOF};
use lumen_core::quic::{endpoint, io, Hello, Start, Welcome}; use punktfunk_core::quic::{endpoint, io, Hello, Start, Welcome};
use lumen_core::transport::UdpTransport; use punktfunk_core::transport::UdpTransport;
use lumen_core::Session; use punktfunk_core::Session;
use rand::RngCore; use rand::RngCore;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
@@ -88,7 +88,7 @@ fn fingerprint_hex(fp: &[u8; 32]) -> String {
/// keeps serving — only endpoint-level failures are fatal. /// keeps serving — only endpoint-level failures are fatal.
async fn serve(opts: M3Options) -> Result<()> { async fn serve(opts: M3Options) -> Result<()> {
let identity = crate::gamestream::cert::ServerIdentity::load_or_create() 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) let fingerprint = endpoint::fingerprint_of_pem(&identity.cert_pem)
.map_err(|e| anyhow!("cert fingerprint: {e}"))?; .map_err(|e| anyhow!("cert fingerprint: {e}"))?;
let ep = endpoint::server_with_identity( let ep = endpoint::server_with_identity(
@@ -101,7 +101,7 @@ async fn serve(opts: M3Options) -> Result<()> {
port = opts.port, port = opts.port,
source = ?opts.source, source = ?opts.source,
fingerprint = %fingerprint_hex(&fingerprint), 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 // 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(); 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 { if let Err(e) = serve_session(conn, &opts, &audio_cap).await {
tracing::warn!(%peer, error = %format!("{e:#}"), "session ended with error"); tracing::warn!(%peer, error = %format!("{e:#}"), "session ended with error");
} else { } else {
@@ -164,10 +164,10 @@ async fn serve_session(
let hello = Hello::decode(&io::read_msg(&mut recv).await?) let hello = Hello::decode(&io::read_msg(&mut recv).await?)
.map_err(|e| anyhow!("Hello decode: {e:?}"))?; .map_err(|e| anyhow!("Hello decode: {e:?}"))?;
anyhow::ensure!( anyhow::ensure!(
hello.abi_version == lumen_core::ABI_VERSION, hello.abi_version == punktfunk_core::ABI_VERSION,
"ABI mismatch: client {} host {}", "ABI mismatch: client {} host {}",
hello.abi_version, hello.abi_version,
lumen_core::ABI_VERSION punktfunk_core::ABI_VERSION
); );
crate::encode::validate_dimensions( crate::encode::validate_dimensions(
crate::encode::Codec::H265, crate::encode::Codec::H265,
@@ -184,10 +184,10 @@ async fn serve_session(
let mut key = [0u8; 16]; let mut key = [0u8; 16];
rand::thread_rng().fill_bytes(&mut key); rand::thread_rng().fill_bytes(&mut key);
let welcome = Welcome { let welcome = Welcome {
abi_version: lumen_core::ABI_VERSION, abi_version: punktfunk_core::ABI_VERSION,
udp_port, udp_port,
mode: hello.mode, 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 { fec: FecConfig {
scheme: FecScheme::Gf16, scheme: FecScheme::Gf16,
fec_percent: 20, fec_percent: 20,
@@ -196,7 +196,7 @@ async fn serve_session(
shard_payload: 1200, shard_payload: 1200,
encrypt: true, encrypt: true,
key, key,
salt: *b"lmn1", salt: *b"pkf1",
frames: match source { frames: match source {
M3Source::Synthetic => frames, M3Source::Synthetic => frames,
M3Source::Virtual => 0, // unbounded — client streams until we close M3Source::Virtual => 0, // unbounded — client streams until we close
@@ -222,7 +222,7 @@ async fn serve_session(
let input_handle = { let input_handle = {
let conn = conn.clone(); let conn = conn.clone();
std::thread::Builder::new() std::thread::Builder::new()
.name("lumen-m3-input".into()) .name("punktfunk-m3-input".into())
.spawn(move || input_thread(input_rx, conn)) .spawn(move || input_thread(input_rx, conn))
.context("spawn input thread")? .context("spawn input thread")?
}; };
@@ -260,7 +260,7 @@ async fn serve_session(
let stop = stop.clone(); let stop = stop.clone();
let cap = audio_cap.clone(); let cap = audio_cap.clone();
std::thread::Builder::new() std::thread::Builder::new()
.name("lumen-m3-audio".into()) .name("punktfunk-m3-audio".into())
.spawn(move || audio_thread(conn, stop, cap)) .spawn(move || audio_thread(conn, stop, cap))
.map_err(|e| tracing::error!(error = %e, "audio thread spawn failed — session continues without audio")) .map_err(|e| tracing::error!(error = %e, "audio thread spawn failed — session continues without audio"))
.ok() .ok()
@@ -313,8 +313,8 @@ async fn serve_session(
result result
} }
/// Per-pad accumulated state: lumen/1 gamepad events are incremental (one button or axis /// Per-pad accumulated state: punktfunk/1 gamepad events are incremental (one button or axis
/// per datagram, see `lumen_core::input::gamepad`), the virtual xpad applies full frames. /// per datagram, see `punktfunk_core::input::gamepad`), the virtual xpad applies full frames.
#[derive(Clone, Copy, Default)] #[derive(Clone, Copy, Default)]
struct PadState { struct PadState {
buttons: u32, buttons: u32,
@@ -337,7 +337,7 @@ impl PadState {
} }
return true; 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 stick = ev.x.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
let trigger = ev.x.clamp(0, 255) as u8; let trigger = ev.x.clamp(0, 255) as u8;
match ev.code { match ev.code {
@@ -403,7 +403,7 @@ fn input_thread(rx: std::sync::mpsc::Receiver<InputEvent>, conn: quinn::Connecti
let backend = crate::inject::default_backend(); let backend = crate::inject::default_backend();
match crate::inject::open(backend) { match crate::inject::open(backend) {
Ok(i) => { Ok(i) => {
tracing::info!(?backend, "lumen/1 input injector opened"); tracing::info!(?backend, "punktfunk/1 input injector opened");
injector = Some(i); injector = Some(i);
} }
Err(e) => { Err(e) => {
@@ -430,14 +430,14 @@ fn input_thread(rx: std::sync::mpsc::Receiver<InputEvent>, conn: quinn::Connecti
*s = (low, high); *s = (low, high);
rumble_seen[pad as usize] = true; 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()); let _ = conn.send_datagram(d.to_vec().into());
}); });
if last_refresh.elapsed() >= std::time::Duration::from_millis(500) { if last_refresh.elapsed() >= std::time::Duration::from_millis(500) {
last_refresh = std::time::Instant::now(); last_refresh = std::time::Instant::now();
for (i, &(low, high)) in rumble_state.iter().enumerate() { for (i, &(low, high)) in rumble_state.iter().enumerate() {
if rumble_seen[i] { 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()); let _ = conn.send_datagram(d.to_vec().into());
} }
} }
@@ -462,7 +462,7 @@ fn audio_thread(conn: quinn::Connection, stop: Arc<AtomicBool>, audio_cap: Audio
None => match crate::audio::open_audio_capture() { None => match crate::audio::open_audio_capture() {
Ok(c) => c, Ok(c) => c,
Err(e) => { 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; return;
} }
}, },
@@ -487,7 +487,7 @@ fn audio_thread(conn: quinn::Connection, stop: Arc<AtomicBool>, audio_cap: Audio
let mut opus_buf = vec![0u8; 1500]; let mut opus_buf = vec![0u8; 1500];
let mut seq: u32 = 0; let mut seq: u32 = 0;
let mut capture_dead = false; 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) { 'session: while !stop.load(Ordering::SeqCst) {
let chunk = match capturer.next_chunk() { let chunk = match capturer.next_chunk() {
Ok(c) => c, Ok(c) => c,
@@ -503,7 +503,8 @@ fn audio_thread(conn: quinn::Connection, stop: Arc<AtomicBool>, audio_cap: Audio
let pts_ns = now_ns(); let pts_ns = now_ns();
match enc.encode_float(&frame, &mut opus_buf) { match enc.encode_float(&frame, &mut opus_buf) {
Ok(n) => { 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() { if conn.send_datagram(d.into()).is_err() {
break 'session; // connection gone break 'session; // connection gone
} }
@@ -520,12 +521,12 @@ fn audio_thread(conn: quinn::Connection, stop: Arc<AtomicBool>, audio_cap: Audio
} }
} }
/// Stub — lumen/1 audio needs Linux (PipeWire capture + libopus); non-Linux dev builds /// Stub — punktfunk/1 audio needs Linux (PipeWire capture + libopus); non-Linux dev builds
/// run sessions without it, same as when the capturer fails to open. /// run sessions without it, same as when the capturer fails to open.
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
fn audio_thread(_conn: quinn::Connection, _stop: Arc<AtomicBool>, _audio_cap: AudioCapSlot) { fn audio_thread(_conn: quinn::Connection, _stop: Arc<AtomicBool>, _audio_cap: AudioCapSlot) {
tracing::warn!( 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(()) 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). /// stamped with the capture wall clock (the client derives per-frame pipeline latency).
fn virtual_stream( fn virtual_stream(
session: &mut Session, session: &mut Session,
mode: lumen_core::Mode, mode: punktfunk_core::Mode,
seconds: u32, seconds: u32,
stop: &AtomicBool, stop: &AtomicBool,
) -> Result<()> { ) -> Result<()> {
let compositor = crate::vdisplay::detect().context("detect compositor")?; 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 mut vd = crate::vdisplay::open(compositor)?;
let vout = vd.create(mode).context("create virtual output")?; let vout = vd.create(mode).context("create virtual output")?;
let mut capturer = let mut capturer =
@@ -600,7 +601,7 @@ fn virtual_stream(
None => next = std::time::Instant::now(), None => next = std::time::Instant::now(),
} }
} }
tracing::info!(sent, "lumen/1 virtual stream complete"); tracing::info!(sent, "punktfunk/1 virtual stream complete");
Ok(()) Ok(())
} }
@@ -622,7 +623,7 @@ mod tests {
/// Incremental wire events accumulate into the full pad frame the virtual xpad applies. /// Incremental wire events accumulate into the full pad frame the virtual xpad applies.
#[test] #[test]
fn gamepad_accumulator() { fn gamepad_accumulator() {
use lumen_core::input::gamepad::*; use punktfunk_core::input::gamepad::*;
let mut s = PadState::default(); let mut s = PadState::default();
assert!(s.apply(&gp(InputKind::GamepadButton, BTN_A, 1, 0))); assert!(s.apply(&gp(InputKind::GamepadButton, BTN_A, 1, 0)));
assert!(s.apply(&gp(InputKind::GamepadButton, BTN_LB, 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_eq!(s.left_trigger, 255);
assert!(!s.apply(&gp(InputKind::GamepadAxis, 42, 1, 0))); 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_A, crate::gamestream::gamepad::BTN_A);
assert_eq!(BTN_GUIDE, crate::gamestream::gamepad::BTN_GUIDE); assert_eq!(BTN_GUIDE, crate::gamestream::gamepad::BTN_GUIDE);
assert_eq!(BTN_DPAD_UP, crate::gamestream::gamepad::BTN_DPAD_UP); assert_eq!(BTN_DPAD_UP, crate::gamestream::gamepad::BTN_DPAD_UP);
} }
/// Pull and byte-verify `count` synthetic frames through the C ABI connection. /// Pull and byte-verify `count` synthetic frames through the C ABI connection.
unsafe fn pull_verified(conn: *mut lumen_core::abi::LumenConnection, count: u32) { unsafe fn pull_verified(conn: *mut punktfunk_core::abi::PunktfunkConnection, count: u32) {
use lumen_core::error::LumenStatus; use punktfunk_core::error::PunktfunkStatus;
let mut got = 0u32; let mut got = 0u32;
let mut frame = unsafe { std::mem::zeroed() }; let mut frame = unsafe { std::mem::zeroed() };
while got < count { while got < count {
match unsafe { lumen_core::abi::lumen_connection_next_au(conn, &mut frame, 2000) } { match unsafe {
LumenStatus::Ok => { 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 data = unsafe { std::slice::from_raw_parts(frame.data, frame.len) };
let idx = u32::from_le_bytes(data[0..4].try_into().unwrap()); let idx = u32::from_le_bytes(data[0..4].try_into().unwrap());
assert_eq!( assert_eq!(
@@ -663,24 +666,24 @@ mod tests {
); );
got += 1; got += 1;
} }
LumenStatus::NoFrame => continue, PunktfunkStatus::NoFrame => continue,
other => panic!("next_au: {other:?}"), other => panic!("next_au: {other:?}"),
} }
} }
} }
/// End-to-end through the C ABI — the exact contract platform clients (Swift) link: /// End-to-end through the C ABI — the exact contract platform clients (Swift) link:
/// in-process lumen/1 host, `lumen_connect` (TOFU → pinned reconnect) → /// in-process punktfunk/1 host, `punktfunk_connect` (TOFU → pinned reconnect) →
/// `lumen_connection_next_au` pulls verified frames → `lumen_connection_send_input` /// `punktfunk_connection_next_au` pulls verified frames → `punktfunk_connection_send_input`
/// enqueues → `lumen_connection_close`. Three sequential sessions against ONE host /// enqueues → `punktfunk_connection_close`. Three sequential sessions against ONE host
/// process prove the persistent listener, and a wrong pin is rejected. /// process prove the persistent listener, and a wrong pin is rejected.
#[test] #[test]
fn c_abi_connection_roundtrip() { fn c_abi_connection_roundtrip() {
use lumen_core::abi::{ use punktfunk_core::abi::{
lumen_connect, lumen_connection_close, lumen_connection_mode, punktfunk_connect, punktfunk_connection_close, punktfunk_connection_mode,
lumen_connection_send_input, punktfunk_connection_send_input,
}; };
use lumen_core::error::LumenStatus; use punktfunk_core::error::PunktfunkStatus;
let host = std::thread::spawn(|| { let host = std::thread::spawn(|| {
run(M3Options { run(M3Options {
@@ -697,7 +700,7 @@ mod tests {
let addr = std::ffi::CString::new("127.0.0.1").unwrap(); let addr = std::ffi::CString::new("127.0.0.1").unwrap();
let mut observed = [0u8; 32]; let mut observed = [0u8; 32];
let conn = unsafe { let conn = unsafe {
lumen_connect( punktfunk_connect(
addr.as_ptr(), addr.as_ptr(),
19777, 19777,
1280, 1280,
@@ -708,20 +711,20 @@ mod tests {
10_000, 10_000,
) )
}; };
assert!(!conn.is_null(), "lumen_connect failed"); assert!(!conn.is_null(), "punktfunk_connect failed");
assert_ne!(observed, [0u8; 32], "fingerprint not reported"); assert_ne!(observed, [0u8; 32], "fingerprint not reported");
let (mut w, mut h, mut hz) = (0u32, 0u32, 0u32); let (mut w, mut h, mut hz) = (0u32, 0u32, 0u32);
assert_eq!( assert_eq!(
unsafe { lumen_connection_mode(conn, &mut w, &mut h, &mut hz) }, unsafe { punktfunk_connection_mode(conn, &mut w, &mut h, &mut hz) },
LumenStatus::Ok PunktfunkStatus::Ok
); );
assert_eq!((w, h, hz), (1280, 720, 60)); assert_eq!((w, h, hz), (1280, 720, 60));
unsafe { pull_verified(conn, 25) }; unsafe { pull_verified(conn, 25) };
let ev = lumen_core::input::InputEvent { let ev = punktfunk_core::input::InputEvent {
kind: lumen_core::input::InputKind::MouseMove, kind: punktfunk_core::input::InputKind::MouseMove,
_pad: [0; 3], _pad: [0; 3],
code: 0, code: 0,
x: 1, x: 1,
@@ -729,14 +732,14 @@ mod tests {
flags: 0, flags: 0,
}; };
assert_eq!( assert_eq!(
unsafe { lumen_connection_send_input(conn, &ev) }, unsafe { punktfunk_connection_send_input(conn, &ev) },
LumenStatus::Ok PunktfunkStatus::Ok
); );
unsafe { lumen_connection_close(conn) }; unsafe { punktfunk_connection_close(conn) };
// Session 2 (same host process — the listener survived): pin the fingerprint. // Session 2 (same host process — the listener survived): pin the fingerprint.
let conn2 = unsafe { let conn2 = unsafe {
lumen_connect( punktfunk_connect(
addr.as_ptr(), addr.as_ptr(),
19777, 19777,
1280, 1280,
@@ -749,12 +752,12 @@ mod tests {
}; };
assert!(!conn2.is_null(), "pinned reconnect failed"); assert!(!conn2.is_null(), "pinned reconnect failed");
unsafe { pull_verified(conn2, 25) }; 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. // Session 3: a wrong pin must be rejected by the handshake.
let bad = [0xAAu8; 32]; let bad = [0xAAu8; 32];
let conn3 = unsafe { let conn3 = unsafe {
lumen_connect( punktfunk_connect(
addr.as_ptr(), addr.as_ptr(),
19777, 19777,
1280, 1280,
@@ -771,7 +774,7 @@ mod tests {
// handshake never yields a connection, so accept() is still waiting. Connect once // 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. // more (TOFU) to complete the host's third session and let it exit.
let conn4 = unsafe { let conn4 = unsafe {
lumen_connect( punktfunk_connect(
addr.as_ptr(), addr.as_ptr(),
19777, 19777,
1280, 1280,
@@ -784,7 +787,7 @@ mod tests {
}; };
assert!(!conn4.is_null()); assert!(!conn4.is_null());
unsafe { pull_verified(conn4, 25) }; unsafe { pull_verified(conn4, 25) };
unsafe { lumen_connection_close(conn4) }; unsafe { punktfunk_connection_close(conn4) };
host.join().unwrap().unwrap(); host.join().unwrap().unwrap();
} }
@@ -1,13 +1,13 @@
//! `lumen-host` — the Linux streaming host (plan §2, §6, §7). //! `punktfunk-host` — the Linux streaming host (plan §2, §6, §7).
//! //!
//! Creates a client-sized virtual display, captures it via PipeWire, encodes with //! 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 //! pacing + send. Input flows back via libei/uinput. The platform backends are
//! `#[cfg(target_os = "linux")]`; the crate compiles everywhere so the workspace builds //! `#[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. //! 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 //! 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. //! Moonlight client connects to.
// Scaffold: trait methods and config paths are defined ahead of their backends. // Scaffold: trait methods and config paths are defined ahead of their backends.
@@ -33,7 +33,7 @@ use m0::{Options, Source};
use std::path::PathBuf; use std::path::PathBuf;
fn main() { 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() tracing_subscriber::fmt()
.with_env_filter( .with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
@@ -48,7 +48,10 @@ fn main() {
} }
fn real_main() -> Result<()> { fn real_main() -> Result<()> {
tracing::info!("lumen-host (lumen_core ABI v{})", lumen_core::ABI_VERSION); tracing::info!(
"punktfunk-host (punktfunk_core ABI v{})",
punktfunk_core::ABI_VERSION
);
let args: Vec<String> = std::env::args().skip(1).collect(); let args: Vec<String> = std::env::args().skip(1).collect();
match args.first().map(String::as_str) { match args.first().map(String::as_str) {
@@ -67,7 +70,7 @@ fn real_main() -> Result<()> {
Some("zerocopy-probe") => zerocopy::probe(), Some("zerocopy-probe") => zerocopy::probe(),
// M0 pipeline spike. // M0 pipeline spike.
Some("m0") => m0::run(parse_m0(&args[1..])?), 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") => { Some("m3-host") => {
let get = |flag: &str| { let get = |flag: &str| {
args.iter() args.iter()
@@ -102,7 +105,7 @@ fn real_main() -> Result<()> {
/// KWin/GNOME, wlr on Sway). Lets us validate input injection without a Moonlight client. /// KWin/GNOME, wlr on Sway). Lets us validate input injection without a Moonlight client.
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn input_test() -> Result<()> { fn input_test() -> Result<()> {
use lumen_core::input::{InputEvent, InputKind}; use punktfunk_core::input::{InputEvent, InputKind};
use std::time::Duration; use std::time::Duration;
let backend = inject::default_backend(); let backend = inject::default_backend();
@@ -188,7 +191,7 @@ fn parse_serve(args: &[String]) -> Result<mgmt::Options> {
} }
// Flag wins over the environment so a unit file can set a default and a shell override it. // Flag wins over the environment so a unit file can set a default and a shell override it.
if opts.token.is_none() { if opts.token.is_none() {
opts.token = std::env::var("LUMEN_MGMT_TOKEN") opts.token = std::env::var("PUNKTFUNK_MGMT_TOKEN")
.ok() .ok()
.filter(|t| !t.is_empty()); .filter(|t| !t.is_empty());
} }
@@ -276,7 +279,7 @@ fn parse_m0(args: &[String]) -> Result<Options> {
Codec::H265 => "h265", Codec::H265 => "h265",
Codec::Av1 => "obu", Codec::Av1 => "obu",
}; };
PathBuf::from(format!("/tmp/lumen-m0.{ext}")) PathBuf::from(format!("/tmp/punktfunk-m0.{ext}"))
}); });
Ok(Options { Ok(Options {
@@ -294,18 +297,18 @@ fn parse_m0(args: &[String]) -> Result<Options> {
fn print_usage() { fn print_usage() {
eprintln!( eprintln!(
"lumen-host — Linux streaming host "punktfunk-host — Linux streaming host
USAGE: 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 + the management REST API
lumen-host openapi print the management API's OpenAPI document (codegen) punktfunk-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) punktfunk-host m3-host [OPTIONS] native punktfunk/1 host (QUIC control plane + UDP data plane)
lumen-host m0 [OPTIONS] M0 captureencodefile pipeline spike punktfunk-host m0 [OPTIONS] M0 captureencodefile pipeline spike
SERVE OPTIONS: SERVE OPTIONS:
--mgmt-bind <IP:PORT> management API address (default: 127.0.0.1:47990) --mgmt-bind <IP:PORT> management API address (default: 127.0.0.1:47990)
--mgmt-token <TOKEN> bearer token for the management API (or LUMEN_MGMT_TOKEN); --mgmt-token <TOKEN> bearer token for the management API (or PUNKTFUNK_MGMT_TOKEN);
required when --mgmt-bind is not loopback required when --mgmt-bind is not loopback
M3-HOST OPTIONS: M3-HOST OPTIONS:
@@ -324,14 +327,14 @@ M0 OPTIONS:
--codec <h264|h265|av1> NVENC codec (default: h265) --codec <h264|h265|av1> NVENC codec (default: h265)
--bitrate <MBPS> target bitrate in Mbps (default: 20) --bitrate <MBPS> target bitrate in Mbps (default: 20)
--width <W> --height <H> synthetic source size (default: 1920x1080) --width <W> --height <H> synthetic source size (default: 1920x1080)
--out <PATH> raw Annex-B output (default: /tmp/lumen-m0.<ext>) --out <PATH> raw Annex-B output (default: /tmp/punktfunk-m0.<ext>)
--no-loopback skip the lumen_core round-trip verification --no-loopback skip the punktfunk_core round-trip verification
-h, --help this help -h, --help this help
NOTES: NOTES:
'portal' needs headless Sway + xdg-desktop-portal-wlr running in this session '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. (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 Encoded AUs are written to a playable file AND (unless --no-loopback) fed through a
lumen_core hostclient loopback that reassembles and byte-verifies each one." punktfunk_core hostclient loopback that reassembles and byte-verifies each one."
); );
} }
@@ -4,12 +4,12 @@
//! the per-frame pipeline never touches this module. //! the per-frame pipeline never touches this module.
//! //!
//! The API is versioned under `/api/v1` and described by an OpenAPI 3.1 document generated //! 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`, //! 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 //! and a copy is checked in at `docs/api/openapi.json` (a test fails if it drifts, like the
//! cbindgen header). //! 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 //! 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 //! non-loopback binds. The OpenAPI document and docs UI are served unauthenticated (the
//! spec is public knowledge — it lives in this repo). //! spec is public knowledge — it lives in this repo).
@@ -73,7 +73,7 @@ pub async fn run(state: Arc<AppState>, opts: Options) -> Result<()> {
let token = opts.token.filter(|t| !t.trim().is_empty()); let token = opts.token.filter(|t| !t.trim().is_empty());
if token.is_none() && !opts.bind.ip().is_loopback() { if token.is_none() && !opts.bind.ip().is_loopback() {
bail!( 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", to expose it beyond this machine",
opts.bind opts.bind
); );
@@ -131,7 +131,7 @@ fn api_router_parts() -> (Router<Arc<MgmtState>>, utoipa::openapi::OpenApi) {
.split_for_parts() .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. /// checked in at `docs/api/openapi.json` for client codegen.
pub fn openapi_json() -> String { pub fn openapi_json() -> String {
let (_, api) = api_router_parts(); let (_, api) = api_router_parts();
@@ -143,8 +143,8 @@ pub fn openapi_json() -> String {
#[derive(OpenApi)] #[derive(OpenApi)]
#[openapi( #[openapi(
info( info(
title = "lumen management API", title = "punktfunk management API",
description = "Control-plane API for managing a lumen streaming host: host \ description = "Control-plane API for managing a punktfunk streaming host: host \
capabilities, runtime status, paired clients, the pairing PIN flow, \ capabilities, runtime status, paired clients, the pairing PIN flow, \
and session control. Authentication: HTTP bearer token, enforced on \ and session control. Authentication: HTTP bearer token, enforced on \
every route except `/api/v1/health` when the host is started with a \ 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. /// Always `"ok"` when the host responds.
#[schema(example = "ok")] #[schema(example = "ok")]
status: String, status: String,
/// `lumen-host` crate version. /// `punktfunk-host` crate version.
version: String, version: String,
/// `lumen-core` C ABI version. /// `punktfunk-core` C ABI version.
abi_version: u32, abi_version: u32,
} }
@@ -205,9 +205,9 @@ struct HostInfo {
uniqueid: String, uniqueid: String,
/// Best-effort primary LAN IP. /// Best-effort primary LAN IP.
local_ip: String, local_ip: String,
/// `lumen-host` crate version. /// `punktfunk-host` crate version.
version: String, version: String,
/// `lumen-core` C ABI version. /// `punktfunk-core` C ABI version.
abi_version: u32, abi_version: u32,
/// GameStream host version advertised to Moonlight clients. /// GameStream host version advertised to Moonlight clients.
app_version: String, app_version: String,
@@ -407,7 +407,7 @@ async fn get_health() -> Json<Health> {
Json(Health { Json(Health {
status: "ok".into(), status: "ok".into(),
version: env!("CARGO_PKG_VERSION").into(), version: env!("CARGO_PKG_VERSION").into(),
abi_version: lumen_core::ABI_VERSION, abi_version: punktfunk_core::ABI_VERSION,
}) })
} }
@@ -429,7 +429,7 @@ async fn get_host_info(State(st): State<Arc<MgmtState>>) -> Json<HostInfo> {
uniqueid: h.uniqueid.clone(), uniqueid: h.uniqueid.clone(),
local_ip: h.local_ip.to_string(), local_ip: h.local_ip.to_string(),
version: env!("CARGO_PKG_VERSION").into(), version: env!("CARGO_PKG_VERSION").into(),
abi_version: lumen_core::ABI_VERSION, abi_version: punktfunk_core::ABI_VERSION,
app_version: APP_VERSION.into(), app_version: APP_VERSION.into(),
gfe_version: GFE_VERSION.into(), gfe_version: GFE_VERSION.into(),
// Everything NVENC encodes here (mirrors SERVER_CODEC_MODE_SUPPORT = 3843). // 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; let (status, body) = send(&app, get_req("/api/v1/health")).await;
assert_eq!(status, StatusCode::OK); assert_eq!(status, StatusCode::OK);
assert_eq!(body["status"], "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] #[tokio::test]
@@ -813,7 +813,7 @@ mod tests {
let (status, body) = send(&app, get_req("/api/v1/clients")).await; let (status, body) = send(&app, get_req("/api/v1/clients")).await;
assert_eq!(status, StatusCode::OK); assert_eq!(status, StatusCode::OK);
assert_eq!(body[0]["fingerprint"], fingerprint); assert_eq!(body[0]["fingerprint"], fingerprint);
assert_eq!(body[0]["subject"], "CN=lumen"); assert_eq!(body[0]["subject"], "CN=punktfunk");
// Malformed fingerprint → 400. // Malformed fingerprint → 400.
let bad = axum::http::Request::delete("/api/v1/clients/zz") let bad = axum::http::Request::delete("/api/v1/clients/zz")
@@ -973,7 +973,7 @@ mod tests {
json.trim(), json.trim(),
checked_in.trim(), checked_in.trim(),
"docs/api/openapi.json is stale — regenerate with: \ "docs/api/openapi.json is stale — regenerate with: \
cargo run -p lumen-host -- openapi > docs/api/openapi.json" cargo run -p punktfunk-host -- openapi > docs/api/openapi.json"
); );
} }
} }
@@ -1,4 +1,4 @@
//! The host hot path (plan §7), wiring the platform stages to `lumen_core`: //! The host hot path (plan §7), wiring the platform stages to `punktfunk_core`:
//! //!
//! ```text //! ```text
//! capture(dmabuf) → encode(NVENC/VAAPI) → core[FEC+packetize+pace+send] //! capture(dmabuf) → encode(NVENC/VAAPI) → core[FEC+packetize+pace+send]
@@ -10,11 +10,11 @@
use crate::capture::Capturer; use crate::capture::Capturer;
use crate::encode::{EncodedFrame, Encoder}; use crate::encode::{EncodedFrame, Encoder};
use anyhow::Result; use anyhow::Result;
use lumen_core::packet::{FLAG_PIC, FLAG_SOF}; use punktfunk_core::packet::{FLAG_PIC, FLAG_SOF};
use lumen_core::Session; use punktfunk_core::Session;
/// Drive one capture→encode→submit step. The real pipeline spawns threads and uses /// 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( pub fn pump_once(
capturer: &mut dyn Capturer, capturer: &mut dyn Capturer,
encoder: &mut dyn Encoder, encoder: &mut dyn Encoder,
@@ -14,7 +14,7 @@
//! consumes the node via [`crate::capture::capture_virtual_output`]. //! consumes the node via [`crate::capture::capture_virtual_output`].
use anyhow::Result; use anyhow::Result;
pub use lumen_core::Mode; pub use punktfunk_core::Mode;
use std::os::fd::OwnedFd; use std::os::fd::OwnedFd;
/// A created virtual output: a PipeWire source to capture, plus an owned keepalive whose drop /// A created virtual output: a PipeWire source to capture, plus an owned keepalive whose drop
@@ -46,7 +46,7 @@ pub trait VirtualDisplay: Send {
fn create(&mut self, mode: Mode) -> Result<VirtualOutput>; fn create(&mut self, mode: Mode) -> Result<VirtualOutput>;
} }
/// Compositors lumen knows how to drive (plan §6). /// Compositors punktfunk knows how to drive (plan §6).
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Compositor { pub enum Compositor {
/// KWin / Plasma 6 — `zkde_screencast` virtual output. /// KWin / Plasma 6 — `zkde_screencast` virtual output.
@@ -59,16 +59,18 @@ pub enum Compositor {
Gamescope, Gamescope,
} }
/// Detect the compositor to drive: `LUMEN_COMPOSITOR` override, else `XDG_CURRENT_DESKTOP`. /// Detect the compositor to drive: `PUNKTFUNK_COMPOSITOR` override, else `XDG_CURRENT_DESKTOP`.
pub fn detect() -> Result<Compositor> { pub fn detect() -> Result<Compositor> {
if let Ok(v) = std::env::var("LUMEN_COMPOSITOR") { if let Ok(v) = std::env::var("PUNKTFUNK_COMPOSITOR") {
return match v.trim().to_ascii_lowercase().as_str() { return match v.trim().to_ascii_lowercase().as_str() {
"kwin" | "kde" | "plasma" => Ok(Compositor::Kwin), "kwin" | "kde" | "plasma" => Ok(Compositor::Kwin),
"wlroots" | "sway" | "hyprland" | "wlr" => Ok(Compositor::Wlroots), "wlroots" | "sway" | "hyprland" | "wlr" => Ok(Compositor::Wlroots),
"mutter" | "gnome" => Ok(Compositor::Mutter), "mutter" | "gnome" => Ok(Compositor::Mutter),
"gamescope" => Ok(Compositor::Gamescope), "gamescope" => Ok(Compositor::Gamescope),
other => { other => {
anyhow::bail!("unknown LUMEN_COMPOSITOR '{other}' (kwin|wlroots|mutter|gamescope)") anyhow::bail!(
"unknown PUNKTFUNK_COMPOSITOR '{other}' (kwin|wlroots|mutter|gamescope)"
)
} }
}; };
} }
@@ -86,7 +88,7 @@ pub fn detect() -> Result<Compositor> {
Ok(Compositor::Wlroots) Ok(Compositor::Wlroots)
} else { } else {
anyhow::bail!( 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"
) )
} }
} }
@@ -35,11 +35,11 @@ impl VirtualDisplay for GamescopeDisplay {
fn create(&mut self, mode: Mode) -> Result<VirtualOutput> { fn create(&mut self, mode: Mode) -> Result<VirtualOutput> {
// Attach to an already-running gamescope (debug / Steam-launched session) instead of // Attach to an already-running gamescope (debug / Steam-launched session) instead of
// spawning one: LUMEN_GAMESCOPE_NODE=<pipewire node id>. // spawning one: PUNKTFUNK_GAMESCOPE_NODE=<pipewire node id>.
if let Ok(id) = std::env::var("LUMEN_GAMESCOPE_NODE") { if let Ok(id) = std::env::var("PUNKTFUNK_GAMESCOPE_NODE") {
let node_id: u32 = id let node_id: u32 = id
.parse() .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"); tracing::info!(node_id, "gamescope: attaching to existing PipeWire node");
return Ok(VirtualOutput { return Ok(VirtualOutput {
node_id, node_id,
@@ -54,7 +54,7 @@ impl VirtualDisplay for GamescopeDisplay {
let node_id = wait_for_node(Duration::from_secs(15)).ok_or_else(|| { let node_id = wait_for_node(Duration::from_secs(15)).ok_or_else(|| {
anyhow!( anyhow!(
"gamescope PipeWire node did not appear within 15s — gamescope may have failed to \ "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!( tracing::info!(
@@ -75,16 +75,17 @@ impl VirtualDisplay for GamescopeDisplay {
/// File where the wrapper below writes gamescope's `LIBEI_SOCKET` (its EIS server socket), /// 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`]. /// read by the libei injector to drive input into the nested app. See [`crate::inject`].
pub const EI_SOCKET_FILE: &str = "/tmp/lumen-gamescope-ei"; pub const EI_SOCKET_FILE: &str = "/tmp/punktfunk-gamescope-ei";
/// Spawn `gamescope --backend headless -W w -H h -r hz -- <app>`. The app comes from /// Spawn `gamescope --backend headless -W w -H h -r hz -- <app>`. The app comes from
/// `LUMEN_GAMESCOPE_APP` (default a no-op that just keeps gamescope alive — set it to a real /// `PUNKTFUNK_GAMESCOPE_APP` (default a no-op that just keeps gamescope alive — set it to a real
/// game/GL app for actual content, e.g. `steam -gamepadui` for the SteamOS-like session). /// 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`] /// 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. /// so the input injector can connect to gamescope's EIS server from outside.
fn spawn(w: u32, h: u32, hz: u32) -> Result<Child> { fn spawn(w: u32, h: u32, hz: u32) -> Result<Child> {
let app = std::env::var("LUMEN_GAMESCOPE_APP").unwrap_or_else(|_| "sleep infinity".to_string()); let app =
std::env::var("PUNKTFUNK_GAMESCOPE_APP").unwrap_or_else(|_| "sleep infinity".to_string());
let _ = std::fs::remove_file(EI_SOCKET_FILE); // stale socket path from a previous session let _ = std::fs::remove_file(EI_SOCKET_FILE); // stale socket path from a previous session
let mut cmd = Command::new("gamescope"); let mut cmd = Command::new("gamescope");
cmd.args(["--backend", "headless"]) cmd.args(["--backend", "headless"])
@@ -101,7 +102,7 @@ fn spawn(w: u32, h: u32, hz: u32) -> Result<Child> {
.args(app.split_whitespace()) .args(app.split_whitespace())
// Prefer the NVIDIA GL vendor for the nested session (harmless on a pure-NVIDIA box). // Prefer the NVIDIA GL vendor for the nested session (harmless on a pure-NVIDIA box).
.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); .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() { if let Ok(log2) = log.try_clone() {
cmd.stdout(Stdio::from(log)).stderr(Stdio::from(log2)); cmd.stdout(Stdio::from(log)).stderr(Stdio::from(log2));
} }
@@ -132,7 +133,7 @@ fn wait_for_node(timeout: Duration) -> Option<u32> {
/// Parse `stream available on node ID: N` from the spawned gamescope's log (ANSI-colored). /// Parse `stream available on node ID: N` from the spawned gamescope's log (ANSI-colored).
fn node_from_log() -> Option<u32> { fn node_from_log() -> Option<u32> {
let log = std::fs::read_to_string("/tmp/lumen-gamescope.log").ok()?; let log = std::fs::read_to_string("/tmp/punktfunk-gamescope.log").ok()?;
for line in log.lines().rev() { for line in log.lines().rev() {
if let Some(pos) = line.find("stream available on node ID:") { if let Some(pos) = line.find("stream available on node ID:") {
let tail = &line[pos + "stream available on node ID:".len()..]; let tail = &line[pos + "stream available on node ID:".len()..];
@@ -53,7 +53,7 @@ use zkde::zkde_screencast_unstable_v1::ZkdeScreencastUnstableV1 as Screencast;
const POINTER_EMBEDDED: u32 = 2; const POINTER_EMBEDDED: u32 = 2;
/// The name we give the created output; KWin exposes it to output-management as `Virtual-<name>`. /// The name we give the created output; KWin exposes it to output-management as `Virtual-<name>`.
const VOUT_NAME: &str = "lumen"; const VOUT_NAME: &str = "punktfunk";
/// Highest interface version we drive. KWin currently advertises 5; we rely on the `created` /// 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. /// 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 stop_thread = stop.clone();
let (width, height) = (mode.width, mode.height); let (width, height) = (mode.width, mode.height);
thread::Builder::new() thread::Builder::new()
.name("lumen-kwin-vout".into()) .name("punktfunk-kwin-vout".into())
.spawn(move || virtual_output_thread(width, height, setup_tx, stop_thread)) .spawn(move || virtual_output_thread(width, height, setup_tx, stop_thread))
.context("spawn KWin virtual-output thread")?; .context("spawn KWin virtual-output thread")?;
@@ -16,7 +16,7 @@
//! //!
//! Requires a running Mutter (`gnome-shell` session, or `gnome-shell --headless` for the //! 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 //! 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 super::{Mode, VirtualDisplay, VirtualOutput};
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
@@ -56,7 +56,7 @@ impl VirtualDisplay for MutterDisplay {
let stop = Arc::new(AtomicBool::new(false)); let stop = Arc::new(AtomicBool::new(false));
let stop_thread = stop.clone(); let stop_thread = stop.clone();
thread::Builder::new() thread::Builder::new()
.name("lumen-mutter-vout".into()) .name("punktfunk-mutter-vout".into())
.spawn(move || session_thread(setup_tx, stop_thread)) .spawn(move || session_thread(setup_tx, stop_thread))
.context("spawn Mutter virtual-output thread")?; .context("spawn Mutter virtual-output thread")?;
@@ -1,6 +1,6 @@
//! Zero-copy capture→encode (plan §9): the PipeWire dmabuf is imported into CUDA via EGL and //! 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 //! 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). //! the runtime fallback (foreign-allocator / no-dmabuf / import failure).
//! //!
//! Pieces: [`cuda`] (driver-API FFI + the shared `CUcontext` + device buffers), [`egl`] (the //! 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 cuda::DeviceBuffer;
pub use egl::{DmabufPlane, EglImporter}; 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 { pub fn enabled() -> bool {
std::env::var("LUMEN_ZEROCOPY") std::env::var("PUNKTFUNK_ZEROCOPY")
.map(|v| matches!(v.trim(), "1" | "true" | "yes" | "on")) .map(|v| matches!(v.trim(), "1" | "true" | "yes" | "on"))
.unwrap_or(false) .unwrap_or(false)
} }
+6 -6
View File
@@ -1,8 +1,8 @@
{ {
"openapi": "3.1.0", "openapi": "3.1.0",
"info": { "info": {
"title": "lumen management API", "title": "punktfunk 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).", "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": { "contact": {
"name": "unom" "name": "unom"
}, },
@@ -393,7 +393,7 @@
"abi_version": { "abi_version": {
"type": "integer", "type": "integer",
"format": "int32", "format": "int32",
"description": "`lumen-core` C ABI version.", "description": "`punktfunk-core` C ABI version.",
"minimum": 0 "minimum": 0
}, },
"status": { "status": {
@@ -403,7 +403,7 @@
}, },
"version": { "version": {
"type": "string", "type": "string",
"description": "`lumen-host` crate version." "description": "`punktfunk-host` crate version."
} }
} }
}, },
@@ -425,7 +425,7 @@
"abi_version": { "abi_version": {
"type": "integer", "type": "integer",
"format": "int32", "format": "int32",
"description": "`lumen-core` C ABI version.", "description": "`punktfunk-core` C ABI version.",
"minimum": 0 "minimum": 0
}, },
"app_version": { "app_version": {
@@ -459,7 +459,7 @@
}, },
"version": { "version": {
"type": "string", "type": "string",
"description": "`lumen-host` crate version." "description": "`punktfunk-host` crate version."
} }
} }
}, },

Some files were not shown because too many files have changed in this diff Show More