Full project rename, decided 2026-06-10: - Crates/binaries: punktfunk-core / punktfunk-host / punktfunk-client-rs. - C ABI: punktfunk_* symbols, Punktfunk* types, include/punktfunk_core.h, PUNKTFUNK_FEATURE_QUIC guard (header regenerated; cbindgen renames updated, incl. PUNKTFUNK_BTN_*/PUNKTFUNK_AXIS_* wire constants). - Protocol: punktfunk/1 — control-plane magic LMN1 → PKF1, nonce salt lmn1 → pkf1. WIRE BREAK: clients must be rebuilt from this revision. - Env knobs: PUNKTFUNK_VIDEO_SOURCE / PUNKTFUNK_COMPOSITOR / PUNKTFUNK_ZEROCOPY / …. - Host config dir: ~/.config/punktfunk (the box's dir was migrated in place — the persistent identity is unchanged, pinned fingerprints stay valid). - Swift package: PunktfunkKit + PunktfunkCore.xcframework + PunktfunkConnection (Sources/PunktfunkClient app + tests renamed with it); build-xcframework.sh updated. - scripts/: 60-punktfunk.rules, punktfunk-host.service; OpenAPI doc regenerated. Also: scripts/headless/run-headless-kde.sh — full headless Plasma bringup. Root cause of "desktop but no apps/settings" over the stream: plasmashell launched without XDG_MENU_PREFIX=plasma-, so the launcher resolved a nonexistent applications.menu and rendered an empty menu. The script sets the complete KDE session env (menu prefix, KDE_FULL_SESSION, session version) and rebuilds ksycoca before starting plasmashell. Gate: 97/97 tests, clippy -D warnings (both feature sets), fmt, C-ABI harness PASS, zero lumen references left outside .git. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,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
@@ -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/
|
||||||
|
|||||||
@@ -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 (~2–4 ms
|
NVENC SDK wrapper (libavcodec only emits whole AUs) — the next big latency lever (~2–4 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
@@ -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
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 5–6.)
|
surface is there; see notes 5–6.)
|
||||||
- **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
|
||||||
|
|||||||
+13
-13
@@ -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))
|
||||||
+3
-3
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+6
-6
@@ -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
-1
@@ -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.
|
||||||
+5
-5
@@ -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] = [:]
|
||||||
// a–z: HID 0x04..0x1D → VK 'A'..'Z'.
|
// a–z: HID 0x04..0x1D → VK 'A'..'Z'.
|
||||||
+48
-48
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+8
-8
@@ -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
|
||||||
+1
-1
@@ -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.
|
||||||
+8
-8
@@ -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))
|
||||||
}
|
}
|
||||||
+7
-7
@@ -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)
|
||||||
+2
-2
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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),
|
||||||
Vendored
Vendored
Vendored
Vendored
Vendored
@@ -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 => {
|
||||||
+2
-2
@@ -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> {
|
||||||
+3
-3
@@ -38,7 +38,7 @@ pub type AudioCapSlot = Arc<std::sync::Mutex<Option<Box<dyn AudioCapturer>>>>;
|
|||||||
/// `gcm_key`/`rikeyid` come from `/launch` and key the AES-CBC payload encryption.
|
/// `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);
|
||||||
+2
-2
@@ -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")?;
|
||||||
+2
-2
@@ -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
-1
@@ -1,4 +1,4 @@
|
|||||||
//! Pairing crypto primitives (control plane only — distinct from `lumen_core`'s AES-GCM
|
//! Pairing crypto primitives (control plane only — distinct from `punktfunk_core`'s AES-GCM
|
||||||
//! data-plane sealing). GameStream pairing uses: AES-128-**ECB** with **no padding**,
|
//! 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`.
|
||||||
+2
-2
@@ -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;
|
||||||
+6
-6
@@ -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
-1
@@ -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.
|
||||||
|
|
||||||
+1
-1
@@ -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 {
|
||||||
+12
-12
@@ -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.
|
||||||
+2
-2
@@ -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⌉` Reed–Solomon parity shards generated by
|
//! FEC (P1.5): each block carries `m = ⌈k·pct/100⌉` Reed–Solomon parity shards generated by
|
||||||
//! `lumen_core::fec::Gf8Coder` (the nanors-compatible Cauchy GF(2⁸) coder). Crucially, RS runs
|
//! `punktfunk_core::fec::Gf8Coder` (the nanors-compatible Cauchy GF(2⁸) coder). Crucially, RS runs
|
||||||
//! over the **whole `blocksize` shard** — Moonlight decodes over `packetSize + 16` bytes from
|
//! 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();
|
||||||
+2
-2
@@ -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 capture→encode→file pipeline spike
|
punktfunk-host m0 [OPTIONS] M0 capture→encode→file 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 host→client loopback that reassembles and byte-verifies each one."
|
punktfunk_core host→client loopback that reassembles and byte-verifies each one."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4,12 +4,12 @@
|
|||||||
//! the per-frame pipeline never touches this module.
|
//! the 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"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+11
-10
@@ -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")?;
|
||||||
|
|
||||||
+2
-2
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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
Reference in New Issue
Block a user