diff --git a/.gitea/workflows/android.yml b/.gitea/workflows/android.yml
index baa9524c..7aab49ea 100644
--- a/.gitea/workflows/android.yml
+++ b/.gitea/workflows/android.yml
@@ -1,4 +1,4 @@
-# Android client CI (Gitea Actions). Builds the Rust JNI core (crates/punktfunk-android) via
+# Android client CI (Gitea Actions). Builds the Rust JNI core (clients/android/native) via
# cargo-ndk for both shipping ABIs and assembles the debug APK (clients/android). Mirrors apple.yml
# but on a Linux runner — the NDK is cross-platform, so no self-hosted host is needed.
#
diff --git a/.gitea/workflows/flatpak.yml b/.gitea/workflows/flatpak.yml
index 6e9e8945..eb755af0 100644
--- a/.gitea/workflows/flatpak.yml
+++ b/.gitea/workflows/flatpak.yml
@@ -26,7 +26,7 @@ on:
# The flatpak is the CLIENT — only rebuild when the client/core/manifest change, not on every
# docs/host push (this is a heavy flatpak-builder run). Tags (v*, the client release) build too.
paths:
- - 'crates/punktfunk-client-linux/**'
+ - 'clients/linux/**'
- 'crates/punktfunk-core/**'
- 'packaging/flatpak/**'
- 'Cargo.lock'
@@ -40,6 +40,8 @@ env:
APP_ID: io.unom.Punktfunk
MANIFEST: packaging/flatpak/io.unom.Punktfunk.yml
PACKAGE: punktfunk-client-flatpak # generic-registry package name
+ REPO_URL: https://flatpak.unom.io # shared unom OSTree repo (reusable across unom apps)
+ DEPLOY_DIR: unom-flatpak # ~/
on unom-1 (compose + ./site tree)
jobs:
build-publish:
diff --git a/.gitea/workflows/windows-msix.yml b/.gitea/workflows/windows-msix.yml
index c8875ea1..1c65866f 100644
--- a/.gitea/workflows/windows-msix.yml
+++ b/.gitea/workflows/windows-msix.yml
@@ -5,7 +5,7 @@
# makeappx/signtool are baked into the runner's daemon env, same as windows.yml.
#
# Registry (public, unom org): https://git.unom.io/unom/-/packages (generic group)
-# Packaging internals: crates/punktfunk-client-windows/packaging/README.md. BOM/MAX_PATH runner
+# Packaging internals: clients/windows/packaging/README.md. BOM/MAX_PATH runner
# gotchas baked into the daemon env + windows.yml: see that workflow.
#
# Versioning — MSIX requires a strictly 4-part numeric version (no ~/- suffixes), so:
@@ -25,7 +25,7 @@ on:
push:
branches: [main]
paths:
- - 'crates/punktfunk-client-windows/**'
+ - 'clients/windows/**'
- 'crates/punktfunk-core/**'
- 'Cargo.lock'
- 'Cargo.toml'
@@ -72,7 +72,7 @@ jobs:
MSIX_CERT_PFX_B64: ${{ secrets.MSIX_CERT_PFX_B64 }}
MSIX_CERT_PASSWORD: ${{ secrets.MSIX_CERT_PASSWORD }}
run: |
- & crates/punktfunk-client-windows/packaging/pack-msix.ps1 `
+ & clients/windows/packaging/pack-msix.ps1 `
-Version $env:MSIX_VERSION -TargetDir C:\t\release -OutDir C:\t\msix
- name: Publish to Gitea generic registry
diff --git a/.gitea/workflows/windows.yml b/.gitea/workflows/windows.yml
index 81961891..5f848ec6 100644
--- a/.gitea/workflows/windows.yml
+++ b/.gitea/workflows/windows.yml
@@ -24,14 +24,14 @@ on:
push:
branches: [main]
paths:
- - 'crates/punktfunk-client-windows/**'
+ - 'clients/windows/**'
- 'crates/punktfunk-core/**'
- 'Cargo.lock'
- 'Cargo.toml'
- '.gitea/workflows/windows.yml'
pull_request:
paths:
- - 'crates/punktfunk-client-windows/**'
+ - 'clients/windows/**'
- 'crates/punktfunk-core/**'
- 'Cargo.lock'
- 'Cargo.toml'
diff --git a/.gitignore b/.gitignore
index 7def844a..77c4586a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,11 @@ xcuserdata/
# Windows App SDK staging by windows-reactor build.rs
/temp/
/winmd/
+
+# Client crate build artifacts (clients moved out of crates/ -> clients/ 2026-06-18)
+/clients/*/target
+/clients/*/*/target
+
+# Python bytecode (e.g. clients/android/ci tooling)
+__pycache__/
+*.pyc
diff --git a/CLAUDE.md b/CLAUDE.md
index f01cc2fd..efd9ef68 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -6,10 +6,10 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc
## Where the work stands
-- **M1 (`punktfunk-core` + C ABI): complete and hardened.** FEC recovery, loopback-under-loss,
+- **Core (`punktfunk-core` + C ABI): complete and hardened.** FEC recovery, loopback-under-loss,
proptests, C ABI harness all green; 13 adversarial-review findings fixed +
regression-tested (`a913042`).
-- **M2 (GameStream host): working end-to-end with a stock Moonlight client.** Validated live
+- **GameStream host: working end-to-end with a stock Moonlight client.** Validated live
on this box: pairing (persists across restarts), serverinfo/applist (app catalog from
`~/.config/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
@@ -28,11 +28,11 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc
socket, wlr protocols on Sway) and **gamepads** (uinput X-Box-360 pads + rumble
back-channel; validated live — pad created/destroyed with the session). Management REST API +
checked-in OpenAPI doc (`mgmt.rs`).
-- **M3 (`punktfunk/1`, the native protocol): full session planes, validated live.** QUIC
+- **Native protocol (`punktfunk/1`): full session planes, validated live.** QUIC
control plane (`punktfunk-core` `quic` feature: Hello{mode}/Welcome{full Config}/Start), data
- plane = the hardened M1 `Session` over raw UDP with **GF(2¹⁶) Leopard FEC + AES-GCM**
+ plane = the hardened core `Session` over raw UDP with **GF(2¹⁶) Leopard FEC + AES-GCM**
(inexpressible in GameStream), host creates the native virtual output at the client's
- requested mode. `m3-host` is a **persistent listener** (sessions back to back;
+ requested mode. `punktfunk1-host` is a **persistent listener** (sessions back to back;
`--max-sessions`). QUIC datagrams carry the side planes, demuxed by first byte: input
0xC8 (incl. **gamepads** — incremental events accumulated into the uinput xpad), **Opus
audio** 0xC9 (48 kHz stereo, 5 ms, host→client), **rumble** 0xCA (host→client). **Trust:**
@@ -41,15 +41,15 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc
ceremony** (host arms pairing and displays a 4-digit PIN; a PAKE binds both cert fingerprints so an
attacker gets one online guess, no offline dictionary attack) — PIN pairing is the default for new
hosts. **TOFU on first connect** (`endpoint::client_pinned`) stays as an explicit host opt-in
- (`m3-host --allow-tofu` / `serve --open`, advertised as `pair=optional`) for fully trusted LANs;
+ (`punktfunk1-host --allow-tofu` / `serve --open`, advertised as `pair=optional`) for fully trusted LANs;
clients only offer the TOFU "Trust" path for a host that advertised `pair=optional`, route every
other new host straight to the PIN ceremony, and on a pinned-fingerprint change force re-pairing
(no re-TOFU shortcut). Clients present persistent identities via QUIC client auth, the host stores
paired fingerprints (`punktfunk1-paired.json`) and gates sessions with `--require-pairing` (the
default; `--allow-tofu`/`--open` accept unpaired clients).
- **LAN auto-discovery**: both `serve --native` and `m3-host` advertise the native service over
+ **LAN auto-discovery**: both `serve --native` and `punktfunk1-host` advertise the native service over
mDNS (`_punktfunk._udp`, `crate::discovery`) with TXT `proto`/`fp`(cert fingerprint to
- pin)/`pair`(required|optional)/`id`; `punktfunk-client-rs --discover` lists hosts, Apple clients
+ pin)/`pair`(required|optional)/`id`; `punktfunk-probe --discover` lists hosts, Apple clients
browse the same service via NWBrowser (validated cross-LAN 2026-06-12).
**Mid-stream mode renegotiation**: `Reconfigure` on the still-open control stream — the
host rebuilds output+encoder at the new mode in ~90 ms while the data plane runs on
@@ -58,7 +58,7 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc
(`ClockProbe`/`ClockEcho`, 8 NTP rounds after `Start`, `clock_offset_ns`) aligns the client to the
host clock, so that latency is now valid **cross-machine** (`skew_corrected=true`) — measured GNOME
box → dev box over the LAN: **p50 1.30 ms** (the −1.57 ms inter-box clock offset removed).
- `punktfunk-client-rs` is the
+ `punktfunk-probe` is the
working reference client (`--pin`, datagram counters, `--input-test` incl. gamepad).
The embeddable connector (`NativeClient`) exposes it all over the C ABI: `punktfunk_connect`
(pin/TOFU) + `next_au`/`next_audio`/`next_rumble`/`next_hidout`/`send_input`/
@@ -69,7 +69,7 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc
## What's left
-1. **M4 — client decode + present: macOS stage 1 done, first light achieved
+1. **Native clients — decode + present: macOS stage 1 done, first light achieved
(2026-06-10).** PunktfunkKit compiles and is tested on macOS (AnnexB → VideoToolbox →
`AVSampleBufferDisplayLayer`, GCMouse/GCKeyboard capture, `PunktfunkClient` app shell);
validated live Mac ↔ this box at 720p60 — vkcube on glass, input injected via gamescope
@@ -85,13 +85,13 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc
Loopback-tested end to end (`PUNKTFUNK_TEST_FEEDBACK=1` scripted burst); DualSense
motion sign/scale derived, not yet live-verified. Tests: `swift test` in
`clients/apple` (unit + real-codec round trip),
- `test-loopback.sh` (Swift client vs synthetic m3-hosts on loopback — runs on macOS;
+ `test-loopback.sh` (Swift client vs synthetic punktfunk1-hosts on loopback — runs on macOS;
includes the pairing ceremony + `--require-pairing` gate),
`RemoteFirstLightTests` (full pipeline over the LAN). See
[`clients/apple/README.md`](clients/apple/README.md). Next: stage 2 presenter
(`VTDecompressionSession` + `CAMetalLayer` frame pacing), glass-to-glass numbers via
`tools/latency-probe` (scaffold), iOS variant.
- **Linux stage 1 done, first light 2026-06-12** (`crates/punktfunk-client-linux`, binary
+ **Linux stage 1 done, first light 2026-06-12** (`clients/linux`, binary
`punktfunk-client`): GTK4/libadwaita shell linking `punktfunk-core` directly (no C ABI;
`NativeClient` is now `Sync` — mutexed plane receivers), mDNS host list, TOFU + SPAKE2
PIN dialogs (identity shared with client-rs), FFmpeg software HEVC decode (LOW_DELAY,
@@ -118,7 +118,7 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc
reconfirm. Next: the stage-2 raw-Wayland
presenter (wp_presentation feedback, tearing-control, Vulkan Video on NVIDIA) —
**wgpu/winit rejected** (no dmabuf import / presentation feedback / shortcuts-inhibit).
- **Windows stage 1 done 2026-06-15** (`crates/punktfunk-client-windows`, binary
+ **Windows stage 1 done 2026-06-15** (`clients/windows`, binary
`punktfunk-client`): pure-Rust **WinUI 3** UI via **windows-reactor** (a declarative React-like
framework backed by WinUI; PR #4499 added the `SwapChainPanel` widget + `set_swap_chain`). The
video is a **`SwapChainPanel`** bound to a **D3D11 composition swapchain** (WARP fallback for
@@ -150,7 +150,7 @@ Low-latency desktop/game streaming stack, Linux-first, with a shared Rust protoc
unpaired clients); clients render TOFU only for a `pair=optional` host and force re-pairing on a
fingerprint change. Next (see roadmap): **delegated pairing approval** (an already-paired device
approves a new one).
-4. **M2 polish**: HDR/10-bit (needs HDR capture + metadata plumbing; `av1_nvenc
+4. **GameStream host polish**: HDR/10-bit (needs HDR capture + metadata plumbing; `av1_nvenc
-highbitdepth 1` already encodes Main10 from 8-bit input on this box),
reconnect-at-new-mode robustness. AV1 negotiation and surround audio are implemented
and unit/live-capture tested — both still need a live Moonlight confirmation (select
@@ -193,9 +193,13 @@ crates/punktfunk-host/
vdisplay/{kwin,gamescope,mutter,wlroots}.rs per-compositor client-sized virtual outputs
zerocopy/{egl,cuda,vulkan}.rs dmabuf → CUDA → NVENC (tiled via EGL/GL, LINEAR via Vulkan)
inject/{libei,wlr,gamepad,dualsense}.rs input backends (uinput xpad + UHID DualSense)
- capture.rs · encode.rs · audio.rs · m0.rs · m3.rs · mgmt.rs · native_pairing.rs
-crates/punktfunk-client-rs/ punktfunk/1 reference client (M3 headless test/measurement tool)
-crates/punktfunk-client-linux/ native Linux client (GTK4/libadwaita · FFmpeg · PipeWire · SDL3)
+ capture.rs · encode.rs · audio.rs · spike.rs · punktfunk1.rs · mgmt.rs · native_pairing.rs
+clients/probe/ punktfunk/1 reference/probe client (headless test/measurement tool)
+clients/linux/ native Linux client (GTK4/libadwaita · FFmpeg · PipeWire · SDL3)
+clients/windows/ native Windows client (WinUI 3 via windows-reactor · D3D11 · WASAPI · SDL3)
+clients/apple/ native macOS/iOS client (Swift · VideoToolbox · GameController)
+clients/android/ native Android client (Kotlin app + native/ Rust JNI core over punktfunk-core)
+clients/decky/ Steam Deck Decky plugin
web/ TanStack web console over the mgmt API (status · devices · pairing)
packaging/ Fedora/Bazzite RPM · bootc · COPR (packaging/bazzite/README.md)
tools/{loss-harness,latency-probe}/ measurement (plan §10)
@@ -215,7 +219,7 @@ include/punktfunk_core.h generated C header
- **FEC is the wall-breaker.** GF(2⁸) (≤255 shards/block, Moonlight-compatible) and GF(2¹⁶)
Leopard (≤65535 shards/block) — punktfunk/1 negotiates the latter, removing the ~1 Gbps
ceiling.
-- **M1 security hardening stays intact**: reassembler bounds attacker-controlled fields
+- **Core security hardening stays intact**: reassembler bounds attacker-controlled fields
before allocating (`ReassemblerLimits`); AES-GCM per-direction nonce salts + seq-as-AAD;
ABI `struct_size` checks. Regression tests exist — keep them green.
- **PipeWire consumer discipline**: our capture streams set `node.dont-reconnect` and tear
@@ -240,8 +244,8 @@ PUNKTFUNK_ZEROCOPY=1 cargo run -rp punktfunk-host -- serve
# punktfunk/1 native loopback test (no Moonlight needed; same env as serve, listener persists
# across sessions — bound it with --max-sessions):
-cargo run -rp punktfunk-host -- m3-host --source virtual --seconds 10 --max-sessions 1
-cargo run -rp punktfunk-client-rs -- --mode 1280x720x120 --out /tmp/a.h265 --input-test # + --pin HEX
+cargo run -rp punktfunk-host -- punktfunk1-host --source virtual --seconds 10 --max-sessions 1
+cargo run -rp punktfunk-probe -- --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
diff --git a/Cargo.lock b/Cargo.lock
index c83919b9..88e147d5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2540,7 +2540,7 @@ dependencies = [
]
[[package]]
-name = "punktfunk-android"
+name = "punktfunk-client-android"
version = "0.0.1"
dependencies = [
"android_logger",
@@ -2571,20 +2571,6 @@ dependencies = [
"tracing-subscriber",
]
-[[package]]
-name = "punktfunk-client-rs"
-version = "0.0.1"
-dependencies = [
- "anyhow",
- "mdns-sd",
- "opus",
- "punktfunk-core",
- "quinn",
- "tokio",
- "tracing",
- "tracing-subscriber",
-]
-
[[package]]
name = "punktfunk-client-windows"
version = "0.0.1"
@@ -2693,6 +2679,20 @@ dependencies = [
"xkbcommon",
]
+[[package]]
+name = "punktfunk-probe"
+version = "0.0.1"
+dependencies = [
+ "anyhow",
+ "mdns-sd",
+ "opus",
+ "punktfunk-core",
+ "quinn",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
+]
+
[[package]]
name = "quick-error"
version = "1.2.3"
diff --git a/Cargo.toml b/Cargo.toml
index a13c62d8..21706c4d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,10 +3,10 @@ resolver = "2"
members = [
"crates/punktfunk-core",
"crates/punktfunk-host",
- "crates/punktfunk-client-rs",
- "crates/punktfunk-client-linux",
- "crates/punktfunk-client-windows",
- "crates/punktfunk-android",
+ "clients/probe",
+ "clients/linux",
+ "clients/windows",
+ "clients/android/native",
"tools/latency-probe",
"tools/loss-harness",
]
diff --git a/README.md b/README.md
index fac34099..6145df75 100644
--- a/README.md
+++ b/README.md
@@ -12,10 +12,10 @@ negotiated extension. See [`docs/implementation-plan.md`](docs/implementation-pl
| Milestone | State |
|-----------|-------|
-| **M1 — `punktfunk-core` + C ABI** | ✅ done & hardened (FEC, packetization, AES-GCM, session, adversarial-review fixes, `punktfunk_core.h`) |
-| **M2 — GameStream host → stock Moonlight** | ✅ live end-to-end: pairing, RTSP, audio, per-client virtual output at native res, GPU zero-copy NVENC, gamepads |
-| **M3 — `punktfunk/1` native protocol** | ✅ validated live: QUIC control + GF(2¹⁶) FEC/AES data plane, SPAKE2 PIN pairing, mid-stream mode renegotiation |
-| **M4 — client decode + present (Apple)** | 🟡 macOS first light: AnnexB→VideoToolbox HEVC on glass + input/pairing over `punktfunk/1` (`clients/apple`); iOS + presenter next |
+| **Core — `punktfunk-core` + C ABI** | ✅ done & hardened (FEC, packetization, AES-GCM, session, adversarial-review fixes, `punktfunk_core.h`) |
+| **GameStream host → stock Moonlight** | ✅ live end-to-end: pairing, RTSP, audio, per-client virtual output at native res, GPU zero-copy NVENC, gamepads |
+| **Native protocol — `punktfunk/1`** | ✅ validated live: QUIC control + GF(2¹⁶) FEC/AES data plane, SPAKE2 PIN pairing, mid-stream mode renegotiation |
+| **Native clients — decode + present** | 🟡 macOS first light: AnnexB→VideoToolbox HEVC on glass + input/pairing over `punktfunk/1` (`clients/apple`); iOS + presenter next |
| **Web console + management API** | ✅ TanStack web console (`web/`) over the OpenAPI mgmt API: host status, paired devices, on-demand native pairing (arm → show PIN) |
The **GameStream host works with a stock Moonlight client** — validated live on NVIDIA
@@ -26,7 +26,7 @@ per-session virtual output (KWin, gamescope, Mutter, Sway backends), encoded wit
**`punktfunk/1`** protocol adds a QUIC control plane and a GF(2¹⁶) Leopard-FEC + AES-GCM data
plane (p50 ~0.8 ms capture→reassembled at 720p120). Its trust model is **SPAKE2 PIN pairing by
default** — a new host requires the PIN ceremony; trust-on-first-use is an explicit host opt-in
-(`m3-host --allow-tofu` / `serve --open`, advertised as `pair=optional`) for fully trusted LANs. Both
+(`punktfunk1-host --allow-tofu` / `serve --open`, advertised as `pair=optional`) for fully trusted LANs. Both
run from **one process** (`serve --native`), managed through a REST API + web console. Builds
against FFmpeg 7 or 8; deployed live on Bazzite. Full status: [`CLAUDE.md`](CLAUDE.md);
roadmap, setup guides & progress: the docs site ([`docs-site/`](docs-site) — Fumadocs;
@@ -55,9 +55,12 @@ Building from source (below) is a fallback.
```
crates/
punktfunk-core/ protocol · FEC · pacing · crypto · quic — the C ABI (lib + cdylib + staticlib)
- punktfunk-host/ Linux host: vdisplay · capture · encode · inject · gamestream · m3 · mgmt · native_pairing
- punktfunk-client-rs/ punktfunk/1 reference client (M3 headless; M4 adds decode+present)
-clients/{apple,android}/ native client scaffolds (import punktfunk_core.h); apple = macOS first light
+ punktfunk-host/ Linux host: vdisplay · capture · encode · inject · gamestream · punktfunk1 · mgmt · native_pairing
+clients/
+ probe/ punktfunk/1 reference/probe client (headless test + latency measurement)
+ linux/ windows/ native desktop clients (Rust: GTK4 / WinUI 3, link punktfunk-core directly)
+ apple/ android/ Swift (macOS+iOS) · Kotlin app + native/ Rust JNI core
+ decky/ Steam Deck Decky plugin
web/ TanStack web console (host status · paired devices · pairing) over the mgmt API
packaging/ Fedora/Bazzite RPM · bootc image · COPR (see packaging/bazzite/README.md)
include/punktfunk_core.h cbindgen-generated C header (checked in)
diff --git a/clients/android/README.md b/clients/android/README.md
index 987613fa..3930da57 100644
--- a/clients/android/README.md
+++ b/clients/android/README.md
@@ -11,7 +11,7 @@ machine, trust logic) instead of re-porting it into Kotlin.
| Side | Owns |
|------|------|
-| **Rust** (`crates/punktfunk-android` → `libpunktfunk_android.so`) | the JNI seam, `NativeClient` (QUIC control + UDP data plane), AnnexB→`AMediaCodec` decode, Opus+Oboe audio, VK keymap, latency math, trust/pairing |
+| **Rust** (`clients/android/native` → `libpunktfunk_android.so`) | the JNI seam, `NativeClient` (QUIC control + UDP data plane), AnnexB→`AMediaCodec` decode, Opus+Oboe audio, VK keymap, latency math, trust/pairing |
| **Kotlin** (`clients/android`) | Compose UI (host grid / settings / stream), `SurfaceView` lifecycle, input capture, `NsdManager` discovery, Keystore identity, permissions |
The single seam is `io.unom.punktfunk.kit.NativeBridge` ⇄ `Java_io_unom_punktfunk_kit_NativeBridge_*`.
@@ -19,7 +19,7 @@ The single seam is `io.unom.punktfunk.kit.NativeBridge` ⇄ `Java_io_unom_punktf
## Layout
```
-crates/punktfunk-android/ Rust cdylib (workspace member)
+clients/android/native/ Rust cdylib (workspace member)
src/lib.rs JNI_OnLoad + abiVersion/coreVersion (native-link proof)
src/session.rs session handle lifecycle (connect/close); plane pumps = TODO
@@ -63,7 +63,7 @@ The debug APK lands in `app/build/outputs/apk/debug/`. The scaffold screen calls
- **Scaffold (done):** Gradle modules, cargo-ndk wiring, JNI native-link proof, phone+TV-installable
manifest. `crates/punktfunk-core` `rcgen` switched to the `ring` backend so the client `.so` is
aws-lc-free.
-- **Next (M4 Android stage 1):** video decode (`AMediaCodec` async → `SurfaceView`), audio
+- **Next (Android stage 1):** video decode (`AMediaCodec` async → `SurfaceView`), audio
(Opus + Oboe + jitter ring), input capture → `send_input`, pairing/identity (Keystore-wrapped),
mDNS discovery, the phone/TV Compose UI. The Rust-side homes are stubbed in
- `crates/punktfunk-android/src/session.rs` with port pointers to `crates/punktfunk-client-linux`.
+ `clients/android/native/src/session.rs` with port pointers to `clients/linux`.
diff --git a/clients/android/kit/build.gradle.kts b/clients/android/kit/build.gradle.kts
index b4af79d4..1e8d278e 100644
--- a/clients/android/kit/build.gradle.kts
+++ b/clients/android/kit/build.gradle.kts
@@ -32,7 +32,7 @@ dependencies {
}
// ------------------------------------------------------------------------------------------------
-// cargo-ndk: cross-compile crates/punktfunk-android into this module's jniLibs// so the
+// cargo-ndk: cross-compile clients/android/native (punktfunk-client-android) into this module's jniLibs// so the
// resulting libpunktfunk_android.so is packaged into the app (and any AAR this module produces).
// NDK r28+ aligns to 16 KB pages by default — no extra linker flags. Prereqs (see clients/android
// /README.md): `cargo install cargo-ndk` + `rustup target add aarch64-linux-android x86_64-linux-android`.
@@ -57,7 +57,7 @@ fun androidSdkDir(): String {
fun registerCargoNdk(taskName: String, release: Boolean) =
tasks.register(taskName) {
group = "rust"
- description = "cargo-ndk build of punktfunk-android (${if (release) "release" else "debug"})"
+ description = "cargo-ndk build of punktfunk-client-android (${if (release) "release" else "debug"})"
workingDir = repoRoot
val sdk = androidSdkDir()
// A GUI Android Studio launch does not source the login shell, so make cargo, the NDK, and
@@ -84,7 +84,7 @@ fun registerCargoNdk(taskName: String, release: Boolean) =
// Link against the minSdk-31 sysroot so libaaudio (API 26+) is found.
"--platform", "31",
"-o", file("src/main/jniLibs").absolutePath,
- "build", "-p", "punktfunk-android",
+ "build", "-p", "punktfunk-client-android",
)
if (release) cmd += "--release"
commandLine(cmd)
diff --git a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/NativeBridge.kt b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/NativeBridge.kt
index 46159216..696c76d7 100644
--- a/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/NativeBridge.kt
+++ b/clients/android/kit/src/main/kotlin/io/unom/punktfunk/kit/NativeBridge.kt
@@ -3,7 +3,7 @@ package io.unom.punktfunk.kit
/**
* The single JNI seam to `libpunktfunk_android.so` (the Rust-heavy client core).
*
- * Symbols are implemented in `crates/punktfunk-android`. This object is intentionally thin —
+ * Symbols are implemented in `clients/android/native`. This object is intentionally thin —
* all protocol logic lives in Rust (`punktfunk-core` + the connector); Kotlin only marshals.
*/
object NativeBridge {
diff --git a/crates/punktfunk-android/Cargo.toml b/clients/android/native/Cargo.toml
similarity index 93%
rename from crates/punktfunk-android/Cargo.toml
rename to clients/android/native/Cargo.toml
index 8685c9f6..c73ee641 100644
--- a/crates/punktfunk-android/Cargo.toml
+++ b/clients/android/native/Cargo.toml
@@ -1,5 +1,5 @@
[package]
-name = "punktfunk-android"
+name = "punktfunk-client-android"
description = "punktfunk Android client — JNI bridge ('nativecore') over punktfunk-core (Rust-heavy client model)"
version.workspace = true
edition.workspace = true
@@ -16,7 +16,7 @@ crate-type = ["cdylib"]
[dependencies]
# The whole protocol/transport/FEC/crypto + the embeddable NativeClient connector. `quic` pulls
# the punktfunk/1 control plane (now ring-only — no aws-lc, see punktfunk-core/Cargo.toml).
-punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
+punktfunk-core = { path = "../../../crates/punktfunk-core", features = ["quic"] }
jni = "0.21"
log = "0.4"
diff --git a/crates/punktfunk-android/src/audio.rs b/clients/android/native/src/audio.rs
similarity index 100%
rename from crates/punktfunk-android/src/audio.rs
rename to clients/android/native/src/audio.rs
diff --git a/crates/punktfunk-android/src/decode.rs b/clients/android/native/src/decode.rs
similarity index 80%
rename from crates/punktfunk-android/src/decode.rs
rename to clients/android/native/src/decode.rs
index 370e0dbf..f4f81c52 100644
--- a/crates/punktfunk-android/src/decode.rs
+++ b/clients/android/native/src/decode.rs
@@ -15,7 +15,7 @@ use punktfunk_core::client::NativeClient;
use punktfunk_core::error::PunktfunkError;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
-use std::time::Duration;
+use std::time::{Duration, Instant};
/// The decode loop. Runs on the `pf-decode` thread until `shutdown` is set or the session closes.
pub fn run(client: Arc, window: NativeWindow, shutdown: Arc) {
@@ -56,6 +56,10 @@ pub fn run(client: Arc, window: NativeWindow, shutdown: Arc = None;
while !shutdown.load(Ordering::Relaxed) {
match client.next_frame(Duration::from_millis(5)) {
Ok(frame) => {
@@ -74,6 +78,24 @@ pub fn run(client: Arc, window: NativeWindow, shutdown: Arc break, // session closed
}
rendered += drain(&codec);
+
+ // Loss recovery: under infinite GOP the only recovery keyframe is one we request. The
+ // reassembler drops unrecoverable AUs (frames_dropped); the decoder then conceals the
+ // reference-missing delta frames that follow and renders them without error, so keying off
+ // a decode error rarely fires. Request an IDR when the drop count climbs, throttled — the
+ // decode stays wedged for several frames until the IDR lands, so requesting every frame
+ // would flood the control stream.
+ let dropped = client.frames_dropped();
+ if dropped > last_dropped {
+ last_dropped = dropped;
+ let now = Instant::now();
+ if last_kf_req.is_none_or(|t| now.duration_since(t) >= Duration::from_millis(100)) {
+ last_kf_req = Some(now);
+ let _ = client.request_keyframe();
+ log::debug!("decode: requested keyframe (loss recovery, dropped={dropped})");
+ }
+ }
+
if fed > 0 && fed % 300 == 0 {
log::info!("decode: fed={fed} rendered={rendered}");
}
diff --git a/crates/punktfunk-android/src/feedback.rs b/clients/android/native/src/feedback.rs
similarity index 100%
rename from crates/punktfunk-android/src/feedback.rs
rename to clients/android/native/src/feedback.rs
diff --git a/crates/punktfunk-android/src/lib.rs b/clients/android/native/src/lib.rs
similarity index 100%
rename from crates/punktfunk-android/src/lib.rs
rename to clients/android/native/src/lib.rs
diff --git a/crates/punktfunk-android/src/mic.rs b/clients/android/native/src/mic.rs
similarity index 100%
rename from crates/punktfunk-android/src/mic.rs
rename to clients/android/native/src/mic.rs
diff --git a/crates/punktfunk-android/src/session.rs b/clients/android/native/src/session.rs
similarity index 99%
rename from crates/punktfunk-android/src/session.rs
rename to clients/android/native/src/session.rs
index 5b9b900c..55142fd3 100644
--- a/crates/punktfunk-android/src/session.rs
+++ b/clients/android/native/src/session.rs
@@ -11,7 +11,7 @@
//! Kotlin side), `nativeConnect` with identity + pin (TOFU / pinned), and `nativePair` (SPAKE2 PIN).
//!
//! TODO(M4 Android stage 1): client→host DualSense rich input (`send_rich_input`), mode
-//! renegotiation. Port the remaining orchestration from `crates/punktfunk-client-linux`.
+//! renegotiation. Port the remaining orchestration from `clients/linux`.
use jni::objects::{JObject, JString};
use jni::sys::{jboolean, jint, jlong};
diff --git a/clients/apple/README.md b/clients/apple/README.md
index e7c6b5e3..a8f50280 100644
--- a/clients/apple/README.md
+++ b/clients/apple/README.md
@@ -20,8 +20,8 @@ full session: video AUs, **Opus audio** (`nextAudio()`), **rumble** (`nextRumble
**DualSense feedback** (`nextHidOutput()` — lightbar, player LEDs, adaptive-trigger
effects), input incl. gamepads + DualSense touchpad/motion (`sendTouchpad`/`sendMotion`),
and **cert pinning + TOFU** (`pinSHA256:`/`hostFingerprint`) — see
-`m3.rs::tests::c_abi_connection_roundtrip` (three sequential sessions: TOFU, pinned
-reconnect, wrong-pin rejection). The host (`punktfunk-host m3-host`) is a persistent listener:
+`punktfunk1.rs::tests::c_abi_connection_roundtrip` (three sequential sessions: TOFU, pinned
+reconnect, wrong-pin rejection). The host (`punktfunk-host punktfunk1-host`) is a persistent listener:
reconnect at will during development.
What's here, all compiled and tested on macOS (Xcode 26.5 / Swift 6.3):
@@ -127,10 +127,10 @@ bash test-loopback.sh # full loopback proof: builds punktfunk
# (synthetic source — runs on macOS), streams
# byte-verified frames into the Swift client
-# against the real host (Linux box, see CLAUDE.md "Running on this box") — m3-host is a
+# against the real host (Linux box, see CLAUDE.md "Running on this box") — punktfunk1-host is a
# persistent listener, reconnect at will:
# PUNKTFUNK_COMPOSITOR=gamescope PUNKTFUNK_GAMESCOPE_APP=vkcube PUNKTFUNK_ZEROCOPY=1 \
-# cargo run -rp punktfunk-host -- m3-host --source virtual --seconds 60
+# cargo run -rp punktfunk-host -- punktfunk1-host --source virtual --seconds 60
PUNKTFUNK_REMOTE_HOST= swift test --filter RemoteFirstLightTests # headless
# (+ PUNKTFUNK_REMOTE_PORT / PUNKTFUNK_REMOTE_COMPOSITOR=gamescope|kwin|… /
# PUNKTFUNK_REMOTE_PIN= for the remote pairing test)
diff --git a/clients/apple/Sources/PunktfunkKit/GamepadFeedback.swift b/clients/apple/Sources/PunktfunkKit/GamepadFeedback.swift
index 8cea563a..464c0828 100644
--- a/clients/apple/Sources/PunktfunkKit/GamepadFeedback.swift
+++ b/clients/apple/Sources/PunktfunkKit/GamepadFeedback.swift
@@ -58,7 +58,13 @@ private final class RumbleRenderer: @unchecked Sendable {
private var controller: GCController?
private var low: Motor?
private var high: Motor?
+ // `broken` latches OFF only for a controller that genuinely has no haptics engine (an Xbox pad
+ // on an OS that doesn't expose rumble through GameController, a Siri Remote) — nothing to retry
+ // until the controller changes. A transient engine failure does NOT latch it; it tears down for
+ // a lazy rebuild instead, so a single hiccup can't kill rumble for the whole session.
private var broken = false
+ /// Last logged active/silent state — for a one-line transition log, not per-event spam.
+ private var wasActive = false
func retarget(_ c: GCController?) {
queue.async {
@@ -70,8 +76,14 @@ private final class RumbleRenderer: @unchecked Sendable {
func apply(low lowAmp: UInt16, high highAmp: UInt16) {
queue.async {
+ let active = lowAmp != 0 || highAmp != 0
+ if active != self.wasActive {
+ self.wasActive = active
+ log.debug(
+ "rumble: \(active ? "active" : "stop", privacy: .public) low=\(lowAmp, privacy: .public) high=\(highAmp, privacy: .public)")
+ }
guard !self.broken else { return }
- if (lowAmp != 0 || highAmp != 0), self.low == nil, self.high == nil {
+ if active, self.low == nil, self.high == nil {
self.setup()
}
if self.high != nil {
@@ -92,7 +104,15 @@ private final class RumbleRenderer: @unchecked Sendable {
/// high = right/light — the Xbox/XInput convention the wire carries); one combined
/// engine otherwise, driven by whichever amplitude is stronger.
private func setup() {
- guard let haptics = controller?.haptics else { return }
+ guard let haptics = controller?.haptics else {
+ // No haptics engine at all — an Xbox controller on an OS/firmware that doesn't expose
+ // rumble through GameController (works on Android via the standard Vibrator path, but
+ // Apple's support is controller/OS-dependent), or a Siri Remote. Nothing to retry until
+ // the controller changes; latch off (retarget clears it) and say so once.
+ log.info("rumble: active controller exposes no haptics engine — rumble unavailable")
+ broken = true
+ return
+ }
let localities = haptics.supportedLocalities
if localities.contains(.leftHandle), localities.contains(.rightHandle) {
low = makeMotor(haptics, .leftHandle)
@@ -100,13 +120,28 @@ private final class RumbleRenderer: @unchecked Sendable {
} else {
low = makeMotor(haptics, .default)
}
- if low == nil && high == nil {
- broken = true // no usable engine (e.g. Siri Remote) — stay silent
+ if low == nil, high == nil {
+ // Haptics present but no engine could be built right now (server busy / a transient
+ // error). Do NOT latch broken — the next nonzero amplitude retries setup().
+ log.warning("rumble: haptics present but engine setup failed — will retry on next rumble")
}
}
private func makeMotor(_ haptics: GCDeviceHaptics, _ locality: GCHapticsLocality) -> Motor? {
guard let engine = haptics.createEngine(withLocality: locality) else { return nil }
+ // The haptic server can stop or reset the engine out from under us — app backgrounding, an
+ // audio-session interruption (a call, Siri, another audio app), or a server crash. Left
+ // unhandled the players go dead and every later rumble throws, latching rumble off for the
+ // rest of the session (the "rumble worked, then went spotty" failure). Tear down on the
+ // serial queue so the next nonzero amplitude lazily rebuilds the engine, instead.
+ engine.stoppedHandler = { [weak self] reason in
+ log.info("rumble: haptic engine stopped (reason \(reason.rawValue, privacy: .public)) — will rebuild")
+ self?.queue.async { self?.teardown() }
+ }
+ engine.resetHandler = { [weak self] in
+ log.info("rumble: haptic engine reset — will rebuild")
+ self?.queue.async { self?.teardown() }
+ }
do {
try engine.start()
let event = CHHapticEvent(
@@ -141,14 +176,19 @@ private final class RumbleRenderer: @unchecked Sendable {
}
motor = m
} catch {
- log.warning("haptic update failed — rumble disabled: \(error, privacy: .public)")
+ // A transient failure (the engine stopped/reset between its handler firing and now).
+ // Tear down so the next nonzero amplitude rebuilds — do NOT latch rumble off for the
+ // session (that was the old "spotty" behaviour).
+ log.warning("rumble: haptic update failed — rebuilding: \(error, privacy: .public)")
teardown()
- broken = true
}
}
private func teardown() {
for m in [low, high].compactMap({ $0 }) {
+ // Drop the handlers before stopping so stop() can't re-enter teardown via stoppedHandler.
+ m.engine.stoppedHandler = nil
+ m.engine.resetHandler = nil
try? m.player.stop(atTime: CHHapticTimeImmediate)
m.engine.stop()
}
diff --git a/clients/apple/Sources/PunktfunkKit/PunktfunkConnection.swift b/clients/apple/Sources/PunktfunkKit/PunktfunkConnection.swift
index eb7fc12c..d500f976 100644
--- a/clients/apple/Sources/PunktfunkKit/PunktfunkConnection.swift
+++ b/clients/apple/Sources/PunktfunkKit/PunktfunkConnection.swift
@@ -362,6 +362,21 @@ public final class PunktfunkConnection {
_ = punktfunk_connection_request_keyframe(h)
}
+ /// Cumulative access units the host→client reassembler dropped as unrecoverable (FEC couldn't
+ /// rebuild them). The video pump polls this and calls `requestKeyframe()` when it climbs — the
+ /// correct loss trigger under the host's infinite GOP, where unrecoverable loss yields
+ /// reference-missing delta frames the decoder *silently conceals* (a frozen / garbage picture,
+ /// no decode error and no `.failed` layer), so a decode-error trigger rarely fires. Monotonic
+ /// for the session; 0 after close. Cheap (an atomic load) — safe to poll every pump iteration.
+ public func framesDropped() -> UInt64 {
+ abiLock.lock()
+ defer { abiLock.unlock() }
+ guard let h = handle, !closeRequested else { return 0 }
+ var out: UInt64 = 0
+ _ = punktfunk_connection_frames_dropped(h, &out)
+ return out
+ }
+
/// The currently active session mode (updated by accepted `requestMode` switches).
public func currentMode() -> (width: UInt32, height: UInt32, refreshHz: UInt32) {
abiLock.lock()
diff --git a/clients/apple/Sources/PunktfunkKit/Stage2Pipeline.swift b/clients/apple/Sources/PunktfunkKit/Stage2Pipeline.swift
index 631fa324..728740a9 100644
--- a/clients/apple/Sources/PunktfunkKit/Stage2Pipeline.swift
+++ b/clients/apple/Sources/PunktfunkKit/Stage2Pipeline.swift
@@ -113,8 +113,21 @@ public final class Stage2Pipeline {
let recovery = recovery
let thread = Thread {
var format: CMVideoFormatDescription?
+ var lastFramesDropped = connection.framesDropped()
while token.isLive {
do {
+ // Loss recovery (the primary recovery path). The reassembler drops unrecoverable
+ // AUs (framesDropped) and the decoder then conceals the reference-missing delta
+ // frames that follow — often rendering them WITHOUT an error callback — so the
+ // onDecodeError trigger rarely fires after a real network blip. Ask the host for
+ // a fresh IDR whenever the drop count climbs (throttled in KeyframeRecovery).
+ // Polled every iteration so a total-loss drought recovers the moment packets
+ // resume and the reassembler counts the gap.
+ let dropped = connection.framesDropped()
+ if dropped > lastFramesDropped {
+ lastFramesDropped = dropped
+ recovery.request()
+ }
guard let au = try connection.nextAU(timeoutMs: 100) else { continue }
onFrame?(au)
if let f = AnnexB.formatDescription(fromIDR: au.data) {
diff --git a/clients/apple/Sources/PunktfunkKit/StreamPump.swift b/clients/apple/Sources/PunktfunkKit/StreamPump.swift
index 42042907..ca976e64 100644
--- a/clients/apple/Sources/PunktfunkKit/StreamPump.swift
+++ b/clients/apple/Sources/PunktfunkKit/StreamPump.swift
@@ -46,27 +46,44 @@ final class StreamPump {
let thread = Thread {
var format: CMVideoFormatDescription?
var lastKeyframeRequest = Date.distantPast
+ var lastFramesDropped = connection.framesDropped()
+ // Coalesced host keyframe request: the decode stays wedged for several frames until
+ // the IDR lands, so requesting on every frame would flood the control stream.
+ func requestKeyframeThrottled() {
+ let now = Date()
+ if now.timeIntervalSince(lastKeyframeRequest) > 0.25 {
+ connection.requestKeyframe()
+ lastKeyframeRequest = now
+ }
+ }
while token.isLive {
do {
+ // Loss recovery (the primary recovery path). Under the host's infinite GOP the
+ // only recovery keyframe is one we request. The reassembler drops unrecoverable
+ // AUs (framesDropped); the decoder then *conceals* the reference-missing delta
+ // frames that follow — a frozen / garbage picture, WITHOUT flipping the layer to
+ // .failed — so the .failed check below rarely fires after a real network blip.
+ // Ask the host for a fresh IDR whenever the drop count climbs. Polled every
+ // iteration (not just per AU) so a total-loss drought still recovers the moment
+ // packets resume and the reassembler counts the gap.
+ let dropped = connection.framesDropped()
+ if dropped > lastFramesDropped {
+ lastFramesDropped = dropped
+ requestKeyframeThrottled()
+ }
guard let au = try connection.nextAU(timeoutMs: 100) else { continue }
onFrame?(au)
if let f = AnnexB.formatDescription(fromIDR: au.data) {
format = f // refreshed on every IDR (mode changes included)
}
if layer.status == .failed {
- // Decode wedged: flush and re-gate on the next in-band parameter sets
- // (resuming with a delta frame can't recover), AND ask the host for a
- // fresh IDR. With the host's infinite GOP the next keyframe could be
- // far off, so without the request the picture stays frozen — the
- // intermittent first-connect freeze. Throttled: the layer stays .failed
- // across several polls until the IDR lands, and one request suffices.
+ // Decode wedged hard (the cold-first-connect case — a lost/corrupt opening
+ // IDR): flush and re-gate on the next in-band parameter sets (resuming with
+ // a delta frame can't recover), AND ask the host for a fresh IDR. Throttled:
+ // the layer stays .failed across several polls until the IDR lands.
layer.flush()
format = AnnexB.formatDescription(fromIDR: au.data)
- let now = Date()
- if now.timeIntervalSince(lastKeyframeRequest) > 0.25 {
- connection.requestKeyframe()
- lastKeyframeRequest = now
- }
+ requestKeyframeThrottled()
}
guard let f = format,
let sample = AnnexB.sampleBuffer(au: au, format: f),
diff --git a/clients/apple/Tests/PunktfunkKitTests/LoopbackIntegrationTests.swift b/clients/apple/Tests/PunktfunkKitTests/LoopbackIntegrationTests.swift
index 0c07b3d7..9cec9a80 100644
--- a/clients/apple/Tests/PunktfunkKitTests/LoopbackIntegrationTests.swift
+++ b/clients/apple/Tests/PunktfunkKitTests/LoopbackIntegrationTests.swift
@@ -1,7 +1,7 @@
// Integration: the Swift wrapper against a real punktfunk/1 host over QUIC + UDP on loopback —
// the Swift twin of punktfunk-host's m3.rs::c_abi_connection_roundtrip, this time through the
// statically linked xcframework. Driven by clients/apple/test-loopback.sh, which builds and
-// starts `punktfunk-host m3-host --source synthetic` and sets PUNKTFUNK_LOOPBACK_PORT.
+// starts `punktfunk-host punktfunk1-host --source synthetic` and sets PUNKTFUNK_LOOPBACK_PORT.
import XCTest
@testable import PunktfunkKit
@@ -11,7 +11,7 @@ final class LoopbackIntegrationTests: XCTestCase {
guard let portStr = ProcessInfo.processInfo.environment["PUNKTFUNK_LOOPBACK_PORT"],
let port = UInt16(portStr)
else {
- throw XCTSkip("needs a running m3-host — use clients/apple/test-loopback.sh")
+ throw XCTSkip("needs a running punktfunk1-host — use clients/apple/test-loopback.sh")
}
let conn = try PunktfunkConnection(
@@ -139,7 +139,7 @@ final class LoopbackIntegrationTests: XCTestCase {
guard let portStr = env["PUNKTFUNK_PAIRING_PORT"], let port = UInt16(portStr),
let pin = env["PUNKTFUNK_PAIRING_PIN"]
else {
- throw XCTSkip("needs an armed m3-host — use clients/apple/test-loopback.sh")
+ throw XCTSkip("needs an armed punktfunk1-host — use clients/apple/test-loopback.sh")
}
let identity = try generateIdentity()
diff --git a/clients/apple/Tests/PunktfunkKitTests/RemoteFirstLightTests.swift b/clients/apple/Tests/PunktfunkKitTests/RemoteFirstLightTests.swift
index 584b0b0c..d7a354d4 100644
--- a/clients/apple/Tests/PunktfunkKitTests/RemoteFirstLightTests.swift
+++ b/clients/apple/Tests/PunktfunkKitTests/RemoteFirstLightTests.swift
@@ -5,7 +5,7 @@
//
// Run (host side, on the Linux box):
// PUNKTFUNK_COMPOSITOR=gamescope PUNKTFUNK_GAMESCOPE_APP=vkcube PUNKTFUNK_ZEROCOPY=1 \
-// punktfunk-host m3-host --source virtual --seconds 120
+// punktfunk-host punktfunk1-host --source virtual --seconds 120
// Then here:
// PUNKTFUNK_REMOTE_HOST=192.168.1.70 swift test --filter RemoteFirstLightTests
@@ -54,7 +54,7 @@ final class RemoteFirstLightTests: XCTestCase {
func testRemoteAudioBothDirections() throws {
let env = ProcessInfo.processInfo.environment
guard let host = env["PUNKTFUNK_REMOTE_HOST"] else {
- throw XCTSkip("set PUNKTFUNK_REMOTE_HOST (and start m3-host --source virtual there)")
+ throw XCTSkip("set PUNKTFUNK_REMOTE_HOST (and start punktfunk1-host --source virtual there)")
}
let port = env["PUNKTFUNK_REMOTE_PORT"].flatMap(UInt16.init) ?? 9777
@@ -106,7 +106,7 @@ final class RemoteFirstLightTests: XCTestCase {
func testRemoteStreamDecodesToPixels() throws {
let env = ProcessInfo.processInfo.environment
guard let host = env["PUNKTFUNK_REMOTE_HOST"] else {
- throw XCTSkip("set PUNKTFUNK_REMOTE_HOST (and start m3-host --source virtual there)")
+ throw XCTSkip("set PUNKTFUNK_REMOTE_HOST (and start punktfunk1-host --source virtual there)")
}
let port = env["PUNKTFUNK_REMOTE_PORT"].flatMap(UInt16.init) ?? 9777
// PUNKTFUNK_REMOTE_COMPOSITOR=kwin|gamescope|… asks the host for a specific
diff --git a/clients/apple/test-loopback.sh b/clients/apple/test-loopback.sh
index 7de42af3..ac444bba 100755
--- a/clients/apple/test-loopback.sh
+++ b/clients/apple/test-loopback.sh
@@ -22,10 +22,10 @@ trap 'kill "${HOST_PID:-}" "${PAIR_PID:-}" 2>/dev/null || true' EXIT
# The open host also scripts a feedback burst (rumble + DualSense hidout) right after the
# handshake, so the Swift test can assert the host→client feedback planes end to end.
HOME="$CFG/open" XDG_CONFIG_HOME="$CFG/open/.config" PUNKTFUNK_TEST_FEEDBACK=1 \
- target/release/punktfunk-host m3-host --port "$PORT" --source synthetic --frames 300 &
+ target/release/punktfunk-host punktfunk1-host --port "$PORT" --source synthetic --frames 300 &
HOST_PID=$!
HOME="$CFG/paired" XDG_CONFIG_HOME="$CFG/paired/.config" \
- target/release/punktfunk-host m3-host --port "$PAIR_PORT" --source synthetic --frames 300 \
+ target/release/punktfunk-host punktfunk1-host --port "$PAIR_PORT" --source synthetic --frames 300 \
--require-pairing >"$PAIR_LOG" 2>&1 &
PAIR_PID=$!
sleep 1
diff --git a/clients/decky/README.md b/clients/decky/README.md
index 6a9b9e86..c1d72216 100644
--- a/clients/decky/README.md
+++ b/clients/decky/README.md
@@ -81,7 +81,7 @@ argv and a clear `client-not-found` error surface to the UI. The child PID is tr
installed and runnable on the Deck — via `.deb`/RPM/flatpak, or symlinked into
`~/.local/bin`.
- **avahi** (`avahi-daemon` + `avahi-browse`) for discovery — present on SteamOS/Bazzite.
-- A punktfunk/1 host on the LAN (`punktfunk-host serve --native` or `m3-host`).
+- A punktfunk/1 host on the LAN (`punktfunk-host serve --native` or `punktfunk1-host`).
## Build
diff --git a/crates/punktfunk-client-linux/Cargo.toml b/clients/linux/Cargo.toml
similarity index 94%
rename from crates/punktfunk-client-linux/Cargo.toml
rename to clients/linux/Cargo.toml
index 3d8b0cb3..23971c19 100644
--- a/crates/punktfunk-client-linux/Cargo.toml
+++ b/clients/linux/Cargo.toml
@@ -15,7 +15,7 @@ path = "src/main.rs"
# Everything is Linux-gated so `cargo build --workspace` stays green on macOS (the Mac
# client lives in clients/apple); on other platforms this builds as a stub binary.
[target.'cfg(target_os = "linux")'.dependencies]
-punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
+punktfunk-core = { path = "../../crates/punktfunk-core", features = ["quic"] }
# UI shell. GraphicsOffload needs GTK ≥ 4.14; black-background ≥ 4.16. AlertDialog/
# PreferencesDialog need libadwaita ≥ 1.5.
diff --git a/crates/punktfunk-client-linux/src/app.rs b/clients/linux/src/app.rs
similarity index 100%
rename from crates/punktfunk-client-linux/src/app.rs
rename to clients/linux/src/app.rs
diff --git a/crates/punktfunk-client-linux/src/audio.rs b/clients/linux/src/audio.rs
similarity index 100%
rename from crates/punktfunk-client-linux/src/audio.rs
rename to clients/linux/src/audio.rs
diff --git a/crates/punktfunk-client-linux/src/discovery.rs b/clients/linux/src/discovery.rs
similarity index 100%
rename from crates/punktfunk-client-linux/src/discovery.rs
rename to clients/linux/src/discovery.rs
diff --git a/crates/punktfunk-client-linux/src/gamepad.rs b/clients/linux/src/gamepad.rs
similarity index 96%
rename from crates/punktfunk-client-linux/src/gamepad.rs
rename to clients/linux/src/gamepad.rs
index d184aa94..ae5f485e 100644
--- a/crates/punktfunk-client-linux/src/gamepad.rs
+++ b/clients/linux/src/gamepad.rs
@@ -536,7 +536,17 @@ fn run(
while let Ok((pad, low, high)) = connector.next_rumble(Duration::ZERO) {
if pad == 0 {
if let Some(p) = w.active_id().and_then(|id| w.opened.get_mut(&id)) {
- let _ = p.set_rumble(low, high, 5_000);
+ // Surface a failed SDL rumble write: a swallowed error here (DualSense not in
+ // the right HIDAPI mode, etc.) reads exactly like "rumble doesn't work". The
+ // host logs the send side on 0xCA, so the two together pinpoint host-game vs
+ // client-render.
+ if let Err(e) = p.set_rumble(low, high, 5_000) {
+ tracing::warn!(low, high, error = %e, "rumble: SDL set_rumble failed");
+ } else {
+ tracing::debug!(low, high, "rumble: rendered");
+ }
+ } else {
+ tracing::debug!(low, high, "rumble: received but no active pad to render");
}
}
}
diff --git a/crates/punktfunk-client-linux/src/keymap.rs b/clients/linux/src/keymap.rs
similarity index 100%
rename from crates/punktfunk-client-linux/src/keymap.rs
rename to clients/linux/src/keymap.rs
diff --git a/crates/punktfunk-client-linux/src/main.rs b/clients/linux/src/main.rs
similarity index 100%
rename from crates/punktfunk-client-linux/src/main.rs
rename to clients/linux/src/main.rs
diff --git a/crates/punktfunk-client-linux/src/session.rs b/clients/linux/src/session.rs
similarity index 100%
rename from crates/punktfunk-client-linux/src/session.rs
rename to clients/linux/src/session.rs
diff --git a/crates/punktfunk-client-linux/src/trust.rs b/clients/linux/src/trust.rs
similarity index 99%
rename from crates/punktfunk-client-linux/src/trust.rs
rename to clients/linux/src/trust.rs
index aad68e19..f43e483b 100644
--- a/crates/punktfunk-client-linux/src/trust.rs
+++ b/clients/linux/src/trust.rs
@@ -1,6 +1,6 @@
//! Client identity, the known-hosts (pinned fingerprint) store, and app settings.
//!
-//! The identity shares `~/.config/punktfunk/client-{cert,key}.pem` with `punktfunk-client-rs`
+//! The identity shares `~/.config/punktfunk/client-{cert,key}.pem` with `punktfunk-probe`
//! so a box pairs once whichever client it uses.
use anyhow::{anyhow, Context, Result};
diff --git a/crates/punktfunk-client-linux/src/ui_hosts.rs b/clients/linux/src/ui_hosts.rs
similarity index 100%
rename from crates/punktfunk-client-linux/src/ui_hosts.rs
rename to clients/linux/src/ui_hosts.rs
diff --git a/crates/punktfunk-client-linux/src/ui_settings.rs b/clients/linux/src/ui_settings.rs
similarity index 100%
rename from crates/punktfunk-client-linux/src/ui_settings.rs
rename to clients/linux/src/ui_settings.rs
diff --git a/crates/punktfunk-client-linux/src/ui_stream.rs b/clients/linux/src/ui_stream.rs
similarity index 100%
rename from crates/punktfunk-client-linux/src/ui_stream.rs
rename to clients/linux/src/ui_stream.rs
diff --git a/crates/punktfunk-client-linux/src/video.rs b/clients/linux/src/video.rs
similarity index 100%
rename from crates/punktfunk-client-linux/src/video.rs
rename to clients/linux/src/video.rs
diff --git a/crates/punktfunk-client-rs/Cargo.toml b/clients/probe/Cargo.toml
similarity index 77%
rename from crates/punktfunk-client-rs/Cargo.toml
rename to clients/probe/Cargo.toml
index cb31c774..9c780abe 100644
--- a/crates/punktfunk-client-rs/Cargo.toml
+++ b/clients/probe/Cargo.toml
@@ -1,6 +1,6 @@
[package]
-name = "punktfunk-client-rs"
-description = "punktfunk reference client (M4): VAAPI decode + wgpu/Vulkan present"
+name = "punktfunk-probe"
+description = "punktfunk reference/probe client: headless punktfunk/1 client for testing + latency measurement"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
@@ -9,7 +9,7 @@ authors.workspace = true
repository.workspace = true
[dependencies]
-punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
+punktfunk-core = { path = "../../crates/punktfunk-core", features = ["quic"] }
quinn = "0.11"
tokio = { version = "1", features = ["rt-multi-thread", "net", "time", "macros"] }
anyhow = "1"
diff --git a/crates/punktfunk-client-rs/src/main.rs b/clients/probe/src/main.rs
similarity index 99%
rename from crates/punktfunk-client-rs/src/main.rs
rename to clients/probe/src/main.rs
index 23a94ca8..6b05b994 100644
--- a/crates/punktfunk-client-rs/src/main.rs
+++ b/clients/probe/src/main.rs
@@ -1,4 +1,4 @@
-//! `punktfunk-client-rs` — the reference client for `punktfunk/1` (M3): QUIC control plane, UDP data
+//! `punktfunk-probe` — the reference client for `punktfunk/1` (M3): QUIC control plane, UDP data
//! plane, input over QUIC datagrams. Two modes, decided by the host's Welcome:
//!
//! * **verification** (`frames > 0`, synthetic host): byte-checks deterministic test frames;
@@ -35,7 +35,7 @@
//! over mDNS, prints each (name, addr:port, pairing requirement, cert fingerprint to pin), and
//! exits without connecting.
//!
-//! Usage: `punktfunk-client-rs [--connect HOST:PORT] [--mode WxHxFPS] [--out FILE] [--input-test]
+//! Usage: `punktfunk-probe [--connect HOST:PORT] [--mode WxHxFPS] [--out FILE] [--input-test]
//! [--pin HEX] [--compositor NAME] [--gamepad NAME] | --discover [SECS]`
//! (M4 adds VAAPI decode + wgpu present on this skeleton.)
@@ -193,7 +193,7 @@ fn parse_args() -> Args {
pin,
remode,
pair: get("--pair").map(String::from),
- name: get("--name").unwrap_or("punktfunk-client-rs").to_string(),
+ name: get("--name").unwrap_or("punktfunk-probe").to_string(),
compositor,
gamepad,
bitrate_kbps: get("--bitrate").and_then(|s| s.parse().ok()).unwrap_or(0),
@@ -337,7 +337,7 @@ fn discover(secs: u64) -> Result<()> {
println!("{row}");
}
println!(
- "\nconnect with: punktfunk-client-rs --connect [--pin | --pair ]"
+ "\nconnect with: punktfunk-probe --connect [--pin | --pair ]"
);
}
Ok(())
diff --git a/crates/punktfunk-client-windows/Cargo.toml b/clients/windows/Cargo.toml
similarity index 94%
rename from crates/punktfunk-client-windows/Cargo.toml
rename to clients/windows/Cargo.toml
index eabab5fb..355ebbb8 100644
--- a/crates/punktfunk-client-windows/Cargo.toml
+++ b/clients/windows/Cargo.toml
@@ -13,13 +13,13 @@ name = "punktfunk-client"
path = "src/main.rs"
# Everything is Windows-gated so `cargo build --workspace` stays green on Linux/macOS (the
-# other native clients live in crates/punktfunk-client-linux and clients/apple); on other
+# other native clients live in clients/linux and clients/apple); on other
# platforms this builds as a stub binary. Mirrors the Linux client's cfg(target_os="linux")
# gating exactly.
[target.'cfg(windows)'.dependencies]
# The protocol core, linked directly (no C ABI) — same as the GTK Linux client. NativeClient
# is Sync (mutexed plane receivers), so it drops into a UI app cleanly.
-punktfunk-core = { path = "../punktfunk-core", features = ["quic"] }
+punktfunk-core = { path = "../../crates/punktfunk-core", features = ["quic"] }
# WinUI 3 UI via windows-reactor (a declarative React-like framework backed by WinUI). Its
# `build.rs` downloads the Windows App SDK NuGets and stages the bootstrap DLL + resources.pri
diff --git a/crates/punktfunk-client-windows/packaging/AppxManifest.xml b/clients/windows/packaging/AppxManifest.xml
similarity index 100%
rename from crates/punktfunk-client-windows/packaging/AppxManifest.xml
rename to clients/windows/packaging/AppxManifest.xml
diff --git a/crates/punktfunk-client-windows/packaging/README.md b/clients/windows/packaging/README.md
similarity index 98%
rename from crates/punktfunk-client-windows/packaging/README.md
rename to clients/windows/packaging/README.md
index 6e261ed6..a92e1d8c 100644
--- a/crates/punktfunk-client-windows/packaging/README.md
+++ b/clients/windows/packaging/README.md
@@ -71,7 +71,7 @@ On the Windows runner / dev VM (MSVC + Windows SDK present), after a release bui
```powershell
cargo build --release -p punktfunk-client-windows
-pwsh -File crates/punktfunk-client-windows/packaging/pack-msix.ps1 `
+pwsh -File clients/windows/packaging/pack-msix.ps1 `
-Version 0.2.0.0 -TargetDir C:\t\release -OutDir C:\t\msix
```
diff --git a/crates/punktfunk-client-windows/packaging/assets/Square150x150Logo.png b/clients/windows/packaging/assets/Square150x150Logo.png
similarity index 100%
rename from crates/punktfunk-client-windows/packaging/assets/Square150x150Logo.png
rename to clients/windows/packaging/assets/Square150x150Logo.png
diff --git a/crates/punktfunk-client-windows/packaging/assets/Square44x44Logo.png b/clients/windows/packaging/assets/Square44x44Logo.png
similarity index 100%
rename from crates/punktfunk-client-windows/packaging/assets/Square44x44Logo.png
rename to clients/windows/packaging/assets/Square44x44Logo.png
diff --git a/crates/punktfunk-client-windows/packaging/assets/Square71x71Logo.png b/clients/windows/packaging/assets/Square71x71Logo.png
similarity index 100%
rename from crates/punktfunk-client-windows/packaging/assets/Square71x71Logo.png
rename to clients/windows/packaging/assets/Square71x71Logo.png
diff --git a/crates/punktfunk-client-windows/packaging/assets/StoreLogo.png b/clients/windows/packaging/assets/StoreLogo.png
similarity index 100%
rename from crates/punktfunk-client-windows/packaging/assets/StoreLogo.png
rename to clients/windows/packaging/assets/StoreLogo.png
diff --git a/crates/punktfunk-client-windows/packaging/pack-msix.ps1 b/clients/windows/packaging/pack-msix.ps1
similarity index 100%
rename from crates/punktfunk-client-windows/packaging/pack-msix.ps1
rename to clients/windows/packaging/pack-msix.ps1
diff --git a/crates/punktfunk-client-windows/packaging/punktfunk-codesign.cer b/clients/windows/packaging/punktfunk-codesign.cer
similarity index 100%
rename from crates/punktfunk-client-windows/packaging/punktfunk-codesign.cer
rename to clients/windows/packaging/punktfunk-codesign.cer
diff --git a/crates/punktfunk-client-windows/src/app.rs b/clients/windows/src/app.rs
similarity index 100%
rename from crates/punktfunk-client-windows/src/app.rs
rename to clients/windows/src/app.rs
diff --git a/crates/punktfunk-client-windows/src/audio.rs b/clients/windows/src/audio.rs
similarity index 100%
rename from crates/punktfunk-client-windows/src/audio.rs
rename to clients/windows/src/audio.rs
diff --git a/crates/punktfunk-client-windows/src/discovery.rs b/clients/windows/src/discovery.rs
similarity index 100%
rename from crates/punktfunk-client-windows/src/discovery.rs
rename to clients/windows/src/discovery.rs
diff --git a/crates/punktfunk-client-windows/src/gamepad.rs b/clients/windows/src/gamepad.rs
similarity index 96%
rename from crates/punktfunk-client-windows/src/gamepad.rs
rename to clients/windows/src/gamepad.rs
index 1fb03c9e..de52e0cd 100644
--- a/crates/punktfunk-client-windows/src/gamepad.rs
+++ b/clients/windows/src/gamepad.rs
@@ -499,7 +499,17 @@ fn run(
while let Ok((pad, low, high)) = connector.next_rumble(Duration::ZERO) {
if pad == 0 {
if let Some(p) = w.active_id().and_then(|id| w.opened.get_mut(&id)) {
- let _ = p.set_rumble(low, high, 5_000);
+ // Surface a failed SDL rumble write: a swallowed error here (DualSense not in
+ // the right HIDAPI mode, etc.) reads exactly like "rumble doesn't work". The
+ // host logs the send side on 0xCA, so the two together pinpoint host-game vs
+ // client-render.
+ if let Err(e) = p.set_rumble(low, high, 5_000) {
+ tracing::warn!(low, high, error = %e, "rumble: SDL set_rumble failed");
+ } else {
+ tracing::debug!(low, high, "rumble: rendered");
+ }
+ } else {
+ tracing::debug!(low, high, "rumble: received but no active pad to render");
}
}
}
diff --git a/crates/punktfunk-client-windows/src/input.rs b/clients/windows/src/input.rs
similarity index 100%
rename from crates/punktfunk-client-windows/src/input.rs
rename to clients/windows/src/input.rs
diff --git a/crates/punktfunk-client-windows/src/main.rs b/clients/windows/src/main.rs
similarity index 97%
rename from crates/punktfunk-client-windows/src/main.rs
rename to clients/windows/src/main.rs
index 2de2252e..66fd6d96 100644
--- a/crates/punktfunk-client-windows/src/main.rs
+++ b/clients/windows/src/main.rs
@@ -83,7 +83,7 @@ fn main() {
}
/// `--headless --connect host[:port] …`: connect from the CLI, count frames, print stats — the
-/// Windows analogue of `punktfunk-client-rs`.
+/// Windows analogue of `punktfunk-probe`.
#[cfg(windows)]
fn run_headless_cli(args: &[String], identity: (String, String)) {
use punktfunk_core::config::{CompositorPref, GamepadPref, Mode};
@@ -241,18 +241,18 @@ fn discover_and_print() {
std::thread::sleep(Duration::from_millis(100));
}
if seen.is_empty() {
- println!(" (none found — is a host running with --native / m3-host?)");
+ println!(" (none found — is a host running with --native / punktfunk1-host?)");
}
}
/// WinUI 3 / Direct3D11 / WASAPI / SDL3 are Windows turf; this stub keeps `cargo build
/// --workspace` green on Linux/macOS (the other native clients live in
-/// crates/punktfunk-client-linux and clients/apple).
+/// clients/linux and clients/apple).
#[cfg(not(windows))]
fn main() {
eprintln!(
"punktfunk-client-windows is Windows-only — the Linux client lives in \
- crates/punktfunk-client-linux, the macOS client in clients/apple"
+ clients/linux, the macOS client in clients/apple"
);
std::process::exit(2);
}
diff --git a/crates/punktfunk-client-windows/src/present.rs b/clients/windows/src/present.rs
similarity index 100%
rename from crates/punktfunk-client-windows/src/present.rs
rename to clients/windows/src/present.rs
diff --git a/crates/punktfunk-client-windows/src/session.rs b/clients/windows/src/session.rs
similarity index 100%
rename from crates/punktfunk-client-windows/src/session.rs
rename to clients/windows/src/session.rs
diff --git a/crates/punktfunk-client-windows/src/trust.rs b/clients/windows/src/trust.rs
similarity index 100%
rename from crates/punktfunk-client-windows/src/trust.rs
rename to clients/windows/src/trust.rs
diff --git a/crates/punktfunk-client-windows/src/video.rs b/clients/windows/src/video.rs
similarity index 100%
rename from crates/punktfunk-client-windows/src/video.rs
rename to clients/windows/src/video.rs
diff --git a/crates/punktfunk-core/Cargo.toml b/crates/punktfunk-core/Cargo.toml
index fac14bfa..47005d81 100644
--- a/crates/punktfunk-core/Cargo.toml
+++ b/crates/punktfunk-core/Cargo.toml
@@ -10,7 +10,7 @@ repository.workspace = true
[lib]
name = "punktfunk_core"
-# `lib` — so punktfunk-host / punktfunk-client-rs / tools link it as a normal Rust crate.
+# `lib` — so punktfunk-host / punktfunk-probe / tools link it as a normal Rust crate.
# `staticlib` — `libpunktfunk_core.a` for the C test harness and static embedding.
# `cdylib` — `libpunktfunk_core.{so,dylib}` for Swift/Kotlin clients via the C ABI.
crate-type = ["lib", "cdylib", "staticlib"]
diff --git a/crates/punktfunk-core/src/abi.rs b/crates/punktfunk-core/src/abi.rs
index 6476d3c1..a2592bb2 100644
--- a/crates/punktfunk-core/src/abi.rs
+++ b/crates/punktfunk-core/src/abi.rs
@@ -1494,6 +1494,35 @@ pub unsafe extern "C" fn punktfunk_connection_request_keyframe(
})
}
+/// Cumulative access units the host→client reassembler dropped as unrecoverable (FEC couldn't
+/// rebuild them). A video loop polls this and calls [`punktfunk_connection_request_keyframe`]
+/// when it climbs — the correct loss trigger under the host's infinite GOP, where unrecoverable
+/// loss yields reference-missing delta frames the decoder *silently conceals* (frozen / garbage
+/// picture, no decode error), so a decode-error trigger rarely fires. Monotonic for the session;
+/// compare against the last observed value. Writes 0 to `out` on a NULL connection.
+///
+/// # Safety
+/// `c` is a valid connection handle; `out` is writable (NULL is skipped).
+#[cfg(feature = "quic")]
+#[no_mangle]
+pub unsafe extern "C" fn punktfunk_connection_frames_dropped(
+ c: *const PunktfunkConnection,
+ out: *mut u64,
+) -> PunktfunkStatus {
+ guard(|| {
+ let c = match unsafe { c.as_ref() } {
+ Some(c) => c,
+ None => return PunktfunkStatus::NullPointer,
+ };
+ unsafe {
+ if !out.is_null() {
+ *out = c.inner.frames_dropped();
+ }
+ }
+ PunktfunkStatus::Ok
+ })
+}
+
/// A speed-test measurement, filled by [`punktfunk_connection_probe_result`]. `done` is 0 until
/// the host's end-of-burst report lands, then 1 (the numbers are final). `throughput_kbps` is the
/// measured goodput to drive a bitrate choice from; `loss_pct` is the delivery loss at that rate.
diff --git a/crates/punktfunk-core/src/client.rs b/crates/punktfunk-core/src/client.rs
index 49e9d864..1f029698 100644
--- a/crates/punktfunk-core/src/client.rs
+++ b/crates/punktfunk-core/src/client.rs
@@ -1,10 +1,10 @@
-//! The embeddable `punktfunk/1` client connector (M4 groundwork), behind the `quic` feature.
+//! The embeddable `punktfunk/1` client connector, behind the `quic` feature.
//!
//! [`NativeClient::connect`] runs the full client side of the protocol — QUIC handshake
//! ([`crate::quic`]), UDP data plane ([`crate::session::Session`] on a native thread), input
//! datagrams — and hands the embedder a dead-simple surface: *pull reassembled access units,
//! push input events*. This is what the platform clients (SwiftUI/VideoToolbox, Android, …)
-//! link via the C ABI (`punktfunk_connect` & co. in [`crate::abi`]); `punktfunk-client-rs` is the
+//! link via the C ABI (`punktfunk_connect` & co. in [`crate::abi`]); `punktfunk-probe` is the
//! Rust-native consumer of the same flow.
//!
//! Threading: one worker thread owns a tokio runtime (QUIC control plane only — design
@@ -166,7 +166,7 @@ pub struct NativeClient {
/// kernel sees a high-QoS thread parked waiting on a lower-QoS one and the Thread Performance
/// Checker flags a priority inversion. Matching the producers to the consumers' QoS removes
/// the inversion without slowing the Swift side. No-op off Apple (the Linux client/host don't
-/// run a QoS scheduler, and `punktfunk-client-rs` doesn't care).
+/// run a QoS scheduler, and `punktfunk-probe` doesn't care).
#[cfg(target_vendor = "apple")]
fn pin_thread_user_interactive() {
// SAFETY: sets only the current thread's QoS class — always valid to call.
diff --git a/crates/punktfunk-core/src/packet.rs b/crates/punktfunk-core/src/packet.rs
index 6cab9290..315179d8 100644
--- a/crates/punktfunk-core/src/packet.rs
+++ b/crates/punktfunk-core/src/packet.rs
@@ -12,7 +12,7 @@
//! `frame_index`↔`frameIndex`, `stream_seq`↔`streamPacketIndex`,
//! (`block_index`,`block_count`)↔the `multiFecBlocks` nibbles, and
//! (`data_shards`,`recovery_shards`,`shard_index`)↔the `fecInfo` bitfield. We carry them
-//! as explicit fields rather than bit-packing; full GameStream wire-exactness is an M2
+//! as explicit fields rather than bit-packing; full GameStream wire-exactness is a GameStream-host
//! concern (it also needs RTP framing + RTSP), this is the coherent internal format.
use crate::config::Config;
diff --git a/crates/punktfunk-core/src/quic.rs b/crates/punktfunk-core/src/quic.rs
index ead08844..8fcf477e 100644
--- a/crates/punktfunk-core/src/quic.rs
+++ b/crates/punktfunk-core/src/quic.rs
@@ -1,4 +1,4 @@
-//! `punktfunk/1` — the native control plane (M3), gated behind the `quic` feature.
+//! `punktfunk/1` — the native control plane, gated behind the `quic` feature.
//!
//! 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
@@ -12,9 +12,9 @@
//!
//! after which both sides bring up a [`crate::session::Session`] over a plain
//! [`UdpTransport`](crate::transport::udp) (native threads, no async) and the host streams.
-//! The Welcome carries everything the M1 core negotiates — FEC scheme (including GF(2¹⁶)
+//! The Welcome carries everything the core negotiates — FEC scheme (including GF(2¹⁶)
//! Leopard, which GameStream can't express), shard sizing, crypto key/salt — so the data
-//! plane is exactly the hardened M1 `Session`.
+//! plane is exactly the hardened core `Session`.
//!
//! Transport security: the host presents a long-lived self-signed certificate
//! ([`endpoint::server_with_identity`]) and the client pins its SHA-256 fingerprint
diff --git a/crates/punktfunk-core/src/session.rs b/crates/punktfunk-core/src/session.rs
index a2ec1c95..ea29b7e2 100644
--- a/crates/punktfunk-core/src/session.rs
+++ b/crates/punktfunk-core/src/session.rs
@@ -31,7 +31,7 @@ pub struct Frame {
/// 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
/// input events are not yet filtered. A sliding-window replay filter keyed on the
-/// authenticated sequence belongs with the pairing/handshake layer (M2); until then,
+/// authenticated sequence belongs with the pairing/handshake layer (the GameStream host); until then,
/// rely on the LAN/VPN transport assumption (plan §1).
pub struct Session {
config: Config,
diff --git a/crates/punktfunk-core/src/transport/udp.rs b/crates/punktfunk-core/src/transport/udp.rs
index 94df11d2..81a96720 100644
--- a/crates/punktfunk-core/src/transport/udp.rs
+++ b/crates/punktfunk-core/src/transport/udp.rs
@@ -3,7 +3,7 @@
//! Send is batched via `sendmmsg` ([`Transport::send_batch`], ≤64/syscall) and recv via `recvmmsg`
//! ([`Transport::recv_batch`], ≤32/syscall into a reused ring) — the 1 Gbps+ syscall lever
//! (~125k → a few-k syscalls/sec at line rate). The host additionally paces each frame's send
-//! across the frame interval (see `m3.rs::paced_submit`) so a real NIC doesn't drop a line-rate
+//! across the frame interval (see `punktfunk1.rs::paced_submit`) so a real NIC doesn't drop a line-rate
//! burst. All three layer on this same [`Transport`] seam (scalar fallbacks for loopback/non-Linux).
use super::Transport;
@@ -397,7 +397,7 @@ impl UdpTransport {
/// Sized for 1 Gbps+: at ~1.2 Gbps on the wire an 8 MB buffer is only ~49 ms of steady state,
/// and a single multi-MB IDR keyframe (~4 MB ≈ 3300 packets) instantly fills most of it. 32 MB
/// gives ~200 ms of headroom and absorbs a keyframe burst without EAGAIN drops. (Paced sending
- /// — `m3.rs::paced_submit` — now spreads a big frame's overflow, so this buffer mostly absorbs
+ /// — `punktfunk1.rs::paced_submit` — now spreads a big frame's overflow, so this buffer mostly absorbs
/// the immediate microburst rather than a whole unpaced frame.)
const TARGET_SOCKBUF: usize = 32 * 1024 * 1024;
diff --git a/crates/punktfunk-core/tests/loopback.rs b/crates/punktfunk-core/tests/loopback.rs
index d4aeb10b..df8cabec 100644
--- a/crates/punktfunk-core/tests/loopback.rs
+++ b/crates/punktfunk-core/tests/loopback.rs
@@ -1,4 +1,4 @@
-//! M1 acceptance: round-trip access units through the full host→client path
+//! Core acceptance: round-trip access units through the full host→client path
//! (packetize → FEC → loopback with simulated loss → recover → reassemble) and assert
//! byte-exact recovery, for both FEC schemes, with and without encryption. Plus
//! property tests over the FEC layer's loss patterns.
diff --git a/crates/punktfunk-host/src/audio.rs b/crates/punktfunk-host/src/audio.rs
index e09b7926..8a73f3f7 100644
--- a/crates/punktfunk-host/src/audio.rs
+++ b/crates/punktfunk-host/src/audio.rs
@@ -8,7 +8,7 @@ use anyhow::Result;
/// Opus/GameStream audio is 48 kHz.
pub const SAMPLE_RATE: u32 = 48_000;
-/// Stereo channel count — the default and the punktfunk/1 (M3) audio plane's fixed layout.
+/// Stereo channel count — the default and the punktfunk/1 audio plane's fixed layout.
pub const CHANNELS: usize = 2;
/// Produces interleaved `f32` PCM at [`SAMPLE_RATE`] in the channel count it was opened
diff --git a/crates/punktfunk-host/src/capture.rs b/crates/punktfunk-host/src/capture.rs
index 46b7d898..f750dcf3 100644
--- a/crates/punktfunk-host/src/capture.rs
+++ b/crates/punktfunk-host/src/capture.rs
@@ -1,4 +1,4 @@
-//! Frame capture (plan §7). On Linux: a PipeWire ScreenCast portal stream. M0 uses the
+//! Frame capture (plan §7). On Linux: a PipeWire ScreenCast portal stream. The spike uses the
//! CPU-copy fallback (the portal delivers a CPU buffer; the encoder uploads it to the GPU
//! internally). Zero-copy dmabuf→NVENC import is deferred (plan §9 risk).
@@ -45,7 +45,7 @@ impl PixelFormat {
}
/// A captured frame. [`format`](Self::format)/dimensions describe the pixels regardless of
-/// where they live — [`payload`](Self::payload) is either a CPU buffer (the M0/fallback path)
+/// where they live — [`payload`](Self::payload) is either a CPU buffer (the spike/fallback path)
/// or a GPU buffer already on the device (the zero-copy path, plan §9).
pub struct CapturedFrame {
pub width: u32,
@@ -103,7 +103,7 @@ pub trait Capturer: Send {
fn set_active(&self, _active: bool) {}
}
-/// A deterministic moving test pattern (BGRx). Lets M0 exercise the encode → file →
+/// A deterministic moving test pattern (BGRx). Lets the spike exercise the encode → file →
/// `punktfunk_core` path with no live capture session, and produces obviously non-static
/// content (a sweeping bar + animated gradient) so the encoded output is verifiable.
pub struct SyntheticCapturer {
diff --git a/crates/punktfunk-host/src/capture/dxgi.rs b/crates/punktfunk-host/src/capture/dxgi.rs
index 6b6c5d5a..88a06a9b 100644
--- a/crates/punktfunk-host/src/capture/dxgi.rs
+++ b/crates/punktfunk-host/src/capture/dxgi.rs
@@ -1319,7 +1319,7 @@ pub struct DuplCapturer {
ever_got_frame: bool,
/// Consecutive rebuilds that produced a BORN-LOST duplication (created OK, but its first
/// AcquireNextFrame instantly returned ACCESS_LOST). On the NORMAL desktop this is the hybrid
- /// reparent/flip storm — once it persists, `acquire` returns Err so the m3 loop cold-rebuilds the
+ /// reparent/flip storm — once it persists, `acquire` returns Err so the punktfunk1 loop cold-rebuilds the
/// whole pipeline (new device/output) instead of spinning on a dead dup forever (the bug where the
/// stream froze on the last frame). Reset to 0 by any real frame. NOT armed on the secure
/// (Winlogon) desktop, where a long static dwell is legitimate and must never end the session.
diff --git a/crates/punktfunk-host/src/capture/wgc_relay.rs b/crates/punktfunk-host/src/capture/wgc_relay.rs
index ddb44360..2b78845b 100644
--- a/crates/punktfunk-host/src/capture/wgc_relay.rs
+++ b/crates/punktfunk-host/src/capture/wgc_relay.rs
@@ -2,7 +2,7 @@
//! docs/windows-secure-desktop.md — step 4).
//!
//! WGC won't activate under the SYSTEM account, so the SYSTEM host can't capture the normal desktop
-//! itself. Instead it spawns `m3-host wgc-helper` in the **interactive user session** (so WGC works)
+//! itself. Instead it spawns `punktfunk-host wgc-helper` in the **interactive user session** (so WGC works)
//! via `CreateProcessAsUserW`, with the helper's **stdout** redirected to an anonymous pipe the host
//! reads. The helper ships framed Annex-B access units; this module parses them back into AUs the
//! host relays onto the live QUIC session (same `EncodedFrame` flow, just sourced over a pipe instead
diff --git a/crates/punktfunk-host/src/encode.rs b/crates/punktfunk-host/src/encode.rs
index d115430d..6a8a50ad 100644
--- a/crates/punktfunk-host/src/encode.rs
+++ b/crates/punktfunk-host/src/encode.rs
@@ -1,5 +1,5 @@
//! Hardware video encode (plan §7). Binds FFmpeg (NVENC); never rewrites codecs.
-//! Low-latency preset, B-frames off. M0 feeds BGRx CPU frames directly — `*_nvenc`
+//! Low-latency preset, B-frames off. The spike feeds BGRx CPU frames directly — `*_nvenc`
//! accepts `bgr0` input and converts to YUV on the GPU, so no host-side swscale is
//! needed (dmabuf zero-copy import is deferred; plan §9).
diff --git a/crates/punktfunk-host/src/gamestream/mod.rs b/crates/punktfunk-host/src/gamestream/mod.rs
index 1016563b..e2dee266 100644
--- a/crates/punktfunk-host/src/gamestream/mod.rs
+++ b/crates/punktfunk-host/src/gamestream/mod.rs
@@ -1,10 +1,10 @@
//! GameStream (P1) control plane — what a stock Moonlight/Artemis client talks to around
//! the media streams: mDNS discovery, the nvhttp serverinfo + pairing HTTP(S) API, RTSP,
//! and the ENet control stream. `tokio`/`axum` live here (control plane, I/O-bound — never
-//! the per-frame hot path; that is `punktfunk_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/gamestream-host-plan.md`.
//!
//! 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 GameStream host task list / plan).
pub mod apps;
// Platform-neutral wire/negotiation logic + the Linux capture/encode pipeline (non-Linux
@@ -149,7 +149,10 @@ impl AppState {
/// QUIC server on `cfg.port` in the same process, sharing one [`crate::native_pairing`] handle with
/// the management API so the web console can arm pairing and show the PIN. `None` = GameStream only
/// (the mgmt API's native endpoints report `enabled: false`).
-pub fn serve(mgmt: crate::mgmt::Options, native: Option) -> Result<()> {
+pub fn serve(
+ mgmt: crate::mgmt::Options,
+ native: Option,
+) -> Result<()> {
let host = Host::detect()?;
let identity = cert::ServerIdentity::load_or_create().context("host certificate")?;
let state = Arc::new(AppState::new(host, identity));
@@ -187,7 +190,7 @@ pub fn serve(mgmt: crate::mgmt::Options, native: Option)
tokio::try_join!(
nvhttp::run(state.clone()),
crate::mgmt::run(state.clone(), mgmt, Some(np.clone())),
- crate::m3::serve(crate::m3::native_serve_opts(&cfg), np),
+ crate::punktfunk1::serve(crate::punktfunk1::native_serve_opts(&cfg), np),
)?;
}
_ => {
diff --git a/crates/punktfunk-host/src/gamestream/stream.rs b/crates/punktfunk-host/src/gamestream/stream.rs
index b2eab420..2831aa01 100644
--- a/crates/punktfunk-host/src/gamestream/stream.rs
+++ b/crates/punktfunk-host/src/gamestream/stream.rs
@@ -1,6 +1,6 @@
//! The video data plane: on RTSP PLAY, learn the client's UDP endpoint (it pings the video
//! port), then run capture → NVENC encode → [`VideoPacketizer`] → UDP send. The source is
-//! either real portal desktop capture (`PUNKTFUNK_VIDEO_SOURCE=portal`, the M0 PipeWire path) or
+//! either real portal desktop capture (`PUNKTFUNK_VIDEO_SOURCE=portal`, the portal PipeWire path) or
//! a synthetic test pattern (default). Runs on its own native thread.
use super::video::{FrameType, VideoPacketizer};
diff --git a/crates/punktfunk-host/src/main.rs b/crates/punktfunk-host/src/main.rs
index 9589883b..5368be66 100644
--- a/crates/punktfunk-host/src/main.rs
+++ b/crates/punktfunk-host/src/main.rs
@@ -6,9 +6,10 @@
//! `#[cfg(target_os = "linux")]`; the crate compiles everywhere so the workspace builds
//! on non-Linux dev machines — it just can't run the pipeline there.
//!
-//! Status: M0. The `m0` subcommand runs the capture→encode→file pipeline spike and feeds
-//! the encoded AUs through a `punktfunk_core` loopback. M2 wires the full P1 host that a stock
-//! Moonlight client connects to.
+//! Subcommands: `serve` runs the GameStream-compatible host + management REST API (and, with
+//! `--native`, the native punktfunk/1 host in-process); `punktfunk1-host` runs the native
+//! punktfunk/1 host standalone; `spike` is a capture→encode→file pipeline dev tool that also
+//! round-trips the encoded AUs through a `punktfunk_core` loopback.
// Scaffold: trait methods and config paths are defined ahead of their backends.
#![allow(dead_code)]
@@ -24,15 +25,15 @@ mod encode;
mod gamestream;
mod inject;
mod library;
-mod m0;
-mod m3;
mod mgmt;
mod mgmt_token;
mod native_pairing;
mod pipeline;
+mod punktfunk1;
mod pwinit;
#[cfg(target_os = "windows")]
mod service;
+mod spike;
mod vdisplay;
#[cfg(target_os = "windows")]
mod wgc_helper;
@@ -41,7 +42,7 @@ mod zerocopy;
use anyhow::{bail, Context, Result};
use encode::Codec;
-use m0::{Options, Source};
+use spike::{Options, Source};
use std::path::PathBuf;
fn main() {
@@ -185,10 +186,10 @@ fn real_main() -> Result<()> {
println!("dualsense-test: done");
Ok(())
}
- // M0 pipeline spike.
- Some("m0") => m0::run(parse_m0(&args[1..])?),
- // M3: native punktfunk/1 host (QUIC control plane + UDP data plane).
- Some("m3-host") => {
+ // Capture→encode→file pipeline spike (dev tool).
+ Some("spike") => spike::run(parse_spike(&args[1..])?),
+ // Native punktfunk/1 host (QUIC control plane + UDP data plane).
+ Some("punktfunk1-host") => {
let get = |flag: &str| {
args.iter()
.skip_while(|a| *a != flag)
@@ -196,10 +197,10 @@ fn real_main() -> Result<()> {
.map(String::as_str)
};
let source = match get("--source") {
- Some("virtual") => m3::M3Source::Virtual,
- _ => m3::M3Source::Synthetic,
+ Some("virtual") => punktfunk1::Punktfunk1Source::Virtual,
+ _ => punktfunk1::Punktfunk1Source::Synthetic,
};
- m3::run(m3::M3Options {
+ punktfunk1::run(punktfunk1::Punktfunk1Options {
port: get("--port").and_then(|s| s.parse().ok()).unwrap_or(9777),
source,
seconds: get("--seconds").and_then(|s| s.parse().ok()).unwrap_or(30),
@@ -209,7 +210,7 @@ fn real_main() -> Result<()> {
.unwrap_or(0),
max_concurrent: get("--max-concurrent")
.and_then(|s| s.parse().ok())
- .unwrap_or(m3::DEFAULT_MAX_CONCURRENT),
+ .unwrap_or(punktfunk1::DEFAULT_MAX_CONCURRENT),
// Secure by default: REQUIRE PIN pairing (reject unpaired clients) unless
// --allow-tofu opts into trust-on-first-use — the host then accepts unpaired
// clients and advertises pair=optional. Pairing is always armed so a PIN is
@@ -259,8 +260,9 @@ fn real_main() -> Result<()> {
print_usage();
Ok(())
}
- // Bare flags (no subcommand) default to the m0 spike for back-compat.
- Some(_) => m0::run(parse_m0(&args)?),
+ // Unknown subcommand → usage. (No implicit default; a bare `punktfunk-host` with no
+ // args hits the None arm above and prints help.)
+ Some(other) => bail!("unknown command '{other}' (try --help)"),
}
}
@@ -320,7 +322,7 @@ fn input_test() -> Result<()> {
/// the native punktfunk/1 host in-process (`--native`, the unified host). Returns the mgmt options
/// and the native host config (`None` = GameStream only). Native pairing is **required by default**
/// (an open host any LAN device can stream from is insecure); `--open` turns it off.
-fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option)> {
+fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option)> {
let mut opts = mgmt::Options::default();
let mut native_port: Option = None;
let mut open = false;
@@ -377,14 +379,14 @@ fn parse_serve(args: &[String]) -> Result<(mgmt::Options, Option Result {
+fn parse_spike(args: &[String]) -> Result {
let mut source = Source::Portal;
let mut width = 1920u32;
let mut height = 1080u32;
@@ -465,7 +467,7 @@ fn parse_m0(args: &[String]) -> Result {
Codec::H265 => "h265",
Codec::Av1 => "obu",
};
- PathBuf::from(format!("/tmp/punktfunk-m0.{ext}"))
+ PathBuf::from(format!("/tmp/punktfunk-spike.{ext}"))
});
Ok(Options {
@@ -486,12 +488,12 @@ fn print_usage() {
"punktfunk-host — Linux streaming host
USAGE:
- punktfunk-host serve [OPTIONS] GameStream host control plane (M2: mDNS + serverinfo …)
- + the management REST API
- punktfunk-host openapi print the management API's OpenAPI document (codegen)
- punktfunk-host m3-host [OPTIONS] native punktfunk/1 host (QUIC control plane + UDP data plane)
- punktfunk-host probe-compositor exit 0 iff the compositor is up + ready (session-bringup gate)
- punktfunk-host m0 [OPTIONS] M0 capture→encode→file pipeline spike
+ punktfunk-host serve [OPTIONS] GameStream host control plane (mDNS + serverinfo …)
+ + the management REST API
+ punktfunk-host openapi print the management API's OpenAPI document (codegen)
+ punktfunk-host punktfunk1-host [OPTIONS] native punktfunk/1 host (QUIC control + UDP data plane)
+ punktfunk-host probe-compositor exit 0 iff the compositor is up + ready (bringup gate)
+ punktfunk-host spike [OPTIONS] capture→encode→file pipeline spike (dev tool)
SERVE OPTIONS:
--mgmt-bind management API address (default: 127.0.0.1:47990)
@@ -503,7 +505,7 @@ SERVE OPTIONS:
--open disable mandatory native pairing (default: pairing REQUIRED —
an open host any LAN device can stream from is insecure)
-M3-HOST OPTIONS:
+PUNKTFUNK1-HOST OPTIONS:
--port QUIC listen port (default: 9777)
--source test frames, or virtual display + NVENC (default: synthetic)
--seconds per-session stream duration, virtual source (default: 30)
@@ -516,7 +518,7 @@ M3-HOST OPTIONS:
unpaired clients and logs a 4-digit pairing PIN at startup;
TOFU without pairing is insecure on a LAN
-M0 OPTIONS:
+SPIKE OPTIONS:
--source
frame source (default: portal). 'kwin-virtual' creates a
KWin virtual output at --width x --height and captures it
@@ -525,7 +527,7 @@ M0 OPTIONS:
--codec NVENC codec (default: h265)
--bitrate target bitrate in Mbps (default: 20)
--width --height synthetic source size (default: 1920x1080)
- --out raw Annex-B output (default: /tmp/punktfunk-m0.)
+ --out raw Annex-B output (default: /tmp/punktfunk-spike.)
--no-loopback skip the punktfunk_core round-trip verification
-h, --help this help
@@ -534,8 +536,8 @@ NOTES:
(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
punktfunk_core host→client loopback that reassembles and byte-verifies each one.
- Both 'serve --native' and 'm3-host' advertise the native service over mDNS
- (_punktfunk._udp) for client auto-discovery — 'punktfunk-client-rs --discover' lists them."
+ Both 'serve --native' and 'punktfunk1-host' advertise the native service over mDNS
+ (_punktfunk._udp) for client auto-discovery — 'punktfunk-probe --discover' lists them."
);
#[cfg(target_os = "windows")]
eprintln!(
diff --git a/crates/punktfunk-host/src/native_pairing.rs b/crates/punktfunk-host/src/native_pairing.rs
index 0092020d..f913f8a0 100644
--- a/crates/punktfunk-host/src/native_pairing.rs
+++ b/crates/punktfunk-host/src/native_pairing.rs
@@ -1,6 +1,6 @@
//! Shared native (`punktfunk/1`) pairing state — the on-demand arming PIN (with expiry) plus the
//! persistent paired-clients store. One [`NativePairing`] handle is shared by the punktfunk/1 QUIC
-//! accept loop ([`crate::m3`]) and the management API ([`crate::mgmt`]), so an operator can **arm
+//! accept loop ([`crate::punktfunk1`]) and the management API ([`crate::mgmt`]), so an operator can **arm
//! pairing and read the PIN from the web console** instead of the service log.
//!
//! The PIN direction is inherent to the SPAKE2 ceremony: the *host* mints the PIN and the *client*
diff --git a/crates/punktfunk-host/src/m3.rs b/crates/punktfunk-host/src/punktfunk1.rs
similarity index 98%
rename from crates/punktfunk-host/src/m3.rs
rename to crates/punktfunk-host/src/punktfunk1.rs
index bed2fdd1..9128d2bd 100644
--- a/crates/punktfunk-host/src/m3.rs
+++ b/crates/punktfunk-host/src/punktfunk1.rs
@@ -1,4 +1,4 @@
-//! M3 — the `punktfunk/1` native host: QUIC control plane + the hardened M1 data plane over UDP.
+//! The `punktfunk/1` native host: QUIC control plane + the hardened core data plane over UDP.
//! This is punktfunk's own protocol, past the GameStream compatibility layer:
//!
//! * the Welcome negotiates **GF(2¹⁶) Leopard FEC** (inexpressible in GameStream) + AES-GCM;
@@ -9,9 +9,9 @@
//! * video frames carry a wall-clock `pts_ns`, so a same-host client measures the full
//! capture→encode→FEC→UDP→reassemble latency per frame.
//!
-//! `punktfunk-host m3-host [--port 9777] [--source synthetic|virtual] [--seconds 30]
+//! `punktfunk-host punktfunk1-host [--port 9777] [--source synthetic|virtual] [--seconds 30]
//! [--frames 300]` serves sessions back to back (one at a time — the virtual output and
-//! encoder are single-tenant); `punktfunk-client-rs --connect host:9777` is the counterpart.
+//! encoder are single-tenant); `punktfunk-probe --connect host:9777` is the counterpart.
//! The data plane runs on native threads (no async on the frame path).
//!
//! Alongside video + input, a session carries **audio** (desktop Opus, 5 ms frames, host →
@@ -37,16 +37,16 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum M3Source {
+pub enum Punktfunk1Source {
/// Deterministic test frames (protocol verification; the client byte-checks them).
Synthetic,
/// Real capture: virtual display at the client's requested mode → NVENC.
Virtual,
}
-pub struct M3Options {
+pub struct Punktfunk1Options {
pub port: u16,
- pub source: M3Source,
+ pub source: Punktfunk1Source,
/// Virtual-source stream duration.
pub seconds: u32,
/// Synthetic-source frame count.
@@ -97,7 +97,7 @@ fn now_ns() -> u64 {
.unwrap_or(0)
}
-pub fn run(opts: M3Options) -> Result<()> {
+pub fn run(opts: Punktfunk1Options) -> Result<()> {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
@@ -138,10 +138,10 @@ pub(crate) struct NativeServe {
/// overflow clients wait in the accept queue. Override with `--max-concurrent`.
pub(crate) const DEFAULT_MAX_CONCURRENT: usize = 4;
-pub(crate) fn native_serve_opts(cfg: &NativeServe) -> M3Options {
- M3Options {
+pub(crate) fn native_serve_opts(cfg: &NativeServe) -> Punktfunk1Options {
+ Punktfunk1Options {
port: cfg.port,
- source: M3Source::Virtual,
+ source: Punktfunk1Source::Virtual,
seconds: 7 * 24 * 3600, // per-session cap; large enough not to cut a live stream
frames: 0,
max_sessions: 0,
@@ -153,7 +153,7 @@ pub(crate) fn native_serve_opts(cfg: &NativeServe) -> M3Options {
}
}
-pub(crate) async fn serve(opts: M3Options, np: Arc) -> Result<()> {
+pub(crate) async fn serve(opts: Punktfunk1Options, np: Arc) -> Result<()> {
let identity = crate::gamestream::cert::ServerIdentity::load_or_create()
.context("load host identity (~/.config/punktfunk)")?;
let fingerprint = endpoint::fingerprint_of_pem(&identity.cert_pem)
@@ -427,7 +427,7 @@ async fn pair_ceremony(
#[allow(clippy::too_many_arguments)]
async fn serve_session(
conn: quinn::Connection,
- opts: &M3Options,
+ opts: &Punktfunk1Options,
audio_cap: &AudioCapSlot,
inj_tx: std::sync::mpsc::Sender,
mic_tx: std::sync::mpsc::Sender>,
@@ -514,7 +514,7 @@ async fn serve_session(
// can report what we'll actually drive. Only the Virtual source has a compositor; the
// synthetic source has no virtual output. Blocking probes → spawn_blocking.
let compositor = match source {
- M3Source::Virtual => {
+ Punktfunk1Source::Virtual => {
let pref = hello.compositor;
Some(
tokio::task::spawn_blocking(move || resolve_compositor(pref))
@@ -522,7 +522,7 @@ async fn serve_session(
.context("resolve compositor task")??,
)
}
- M3Source::Synthetic => None,
+ Punktfunk1Source::Synthetic => None,
};
// Resolve a requested library launch (the client sends only the store-qualified id;
@@ -600,8 +600,8 @@ async fn serve_session(
key,
salt: *b"pkf1",
frames: match source {
- M3Source::Synthetic => frames,
- M3Source::Virtual => 0, // unbounded — client streams until we close
+ Punktfunk1Source::Synthetic => frames,
+ Punktfunk1Source::Virtual => 0, // unbounded — client streams until we close
},
// Report the resolved backends back to the client (compositor: Auto for the
// synthetic source).
@@ -726,7 +726,7 @@ async fn serve_session(
let conn = conn.clone();
let gamepad = welcome.gamepad;
std::thread::Builder::new()
- .name("punktfunk-m3-input".into())
+ .name("punktfunk1-input".into())
.spawn(move || input_thread(input_rx, rich_rx, conn, inj_tx, gamepad))
.context("spawn input thread")?
};
@@ -778,12 +778,12 @@ async fn serve_session(
// → host→client QUIC datagrams, on its own native thread. Best-effort on every failure
// (no PipeWire audio, spawn error): the session continues without audio — and a spawn
// error must NOT early-return here, the threads above are already running.
- let audio_handle = if opts.source == M3Source::Virtual {
+ let audio_handle = if opts.source == Punktfunk1Source::Virtual {
let conn = conn.clone();
let stop = stop.clone();
let cap = audio_cap.clone();
std::thread::Builder::new()
- .name("punktfunk-m3-audio".into())
+ .name("punktfunk1-audio".into())
.spawn(move || audio_thread(conn, stop, cap))
.map_err(|e| tracing::error!(error = %e, "audio thread spawn failed — session continues without audio"))
.ok()
@@ -794,7 +794,7 @@ async fn serve_session(
// Test hook (synthetic source only): a scripted feedback burst on the host→client
// planes — rumble (0xCA) + DualSense HID-output (0xCD) — so loopback tests can assert
// the client's feedback path without a real game writing output reports to a real pad.
- if opts.source == M3Source::Synthetic
+ if opts.source == Punktfunk1Source::Synthetic
&& std::env::var("PUNKTFUNK_TEST_FEEDBACK").as_deref() == Ok("1")
{
use punktfunk_core::quic::HidOutput;
@@ -852,14 +852,14 @@ async fn serve_session(
let mut session = Session::new(cfg, Box::new(transport))
.map_err(|e| anyhow!("host session: {e:?}"))?;
match source {
- M3Source::Synthetic => synthetic_stream(
+ Punktfunk1Source::Synthetic => synthetic_stream(
&mut session,
frames,
&stop_stream,
&probe_rx,
&probe_result_tx,
),
- M3Source::Virtual => {
+ Punktfunk1Source::Virtual => {
let compositor = compositor
.expect("the Virtual source resolves a compositor during the handshake");
virtual_stream(
@@ -986,7 +986,7 @@ impl InjectorService {
fn start() -> InjectorService {
let (tx, rx) = std::sync::mpsc::channel::();
if let Err(e) = std::thread::Builder::new()
- .name("punktfunk-m3-injector".into())
+ .name("punktfunk1-injector".into())
.spawn(move || injector_service_thread(rx))
{
tracing::error!(error = %e, "injector service thread spawn failed — pointer/keyboard input disabled");
@@ -1080,7 +1080,7 @@ impl MicService {
fn start() -> MicService {
let (tx, rx) = std::sync::mpsc::channel::>();
if let Err(e) = std::thread::Builder::new()
- .name("punktfunk-m3-mic".into())
+ .name("punktfunk1-mic".into())
.spawn(move || mic_service_thread(rx))
{
tracing::error!(error = %e, "mic service thread spawn failed — mic passthrough disabled");
@@ -2117,7 +2117,7 @@ fn virtual_stream(
let _watcher = if watch {
let stop = stop.clone();
std::thread::Builder::new()
- .name("punktfunk-m3-watcher".into())
+ .name("punktfunk1-watcher".into())
.spawn(move || session_watcher_loop(session_tx, stop))
.ok()
} else {
@@ -3014,9 +3014,9 @@ mod tests {
use punktfunk_core::error::PunktfunkStatus;
let host = std::thread::spawn(|| {
- run(M3Options {
+ run(Punktfunk1Options {
port: 19777,
- source: M3Source::Synthetic,
+ source: Punktfunk1Source::Synthetic,
seconds: 0,
frames: 25,
max_sessions: 3,
@@ -3182,9 +3182,9 @@ mod tests {
.build()
.unwrap();
rt.block_on(serve(
- M3Options {
+ Punktfunk1Options {
port: 19779,
- source: M3Source::Synthetic,
+ source: Punktfunk1Source::Synthetic,
seconds: 0,
frames: 25,
max_sessions: 2, // the knock + the post-approval session
@@ -3268,9 +3268,9 @@ mod tests {
use punktfunk_core::quic::endpoint;
let host = std::thread::spawn(|| {
- run(M3Options {
+ run(Punktfunk1Options {
port: 19778,
- source: M3Source::Synthetic,
+ source: Punktfunk1Source::Synthetic,
seconds: 0,
frames: 25,
max_sessions: 4,
diff --git a/crates/punktfunk-host/src/m0.rs b/crates/punktfunk-host/src/spike.rs
similarity index 94%
rename from crates/punktfunk-host/src/m0.rs
rename to crates/punktfunk-host/src/spike.rs
index a7298f03..e24543ed 100644
--- a/crates/punktfunk-host/src/m0.rs
+++ b/crates/punktfunk-host/src/spike.rs
@@ -1,9 +1,9 @@
-//! M0 — the pipeline spike (plan §8): capture → NVENC encode → playable file, with the
+//! The pipeline spike (plan §8): capture → NVENC encode → playable file, with the
//! encoded access units also fed through a `punktfunk_core` host→client `Session` over an
//! in-process loopback to prove the core's FEC + packetize + reassemble path on real
//! encoder output.
//!
-//! This is the spike runner, not the M2 hot path: it drives the stages on one thread (the
+//! This is the spike runner, not the production host path: it drives the stages on one thread (the
//! per-stage-thread pipeline with bounded channels is [`crate::pipeline`]). Source is
//! either a synthetic BGRx test pattern (no capture session needed) or the live xdg
//! ScreenCast portal monitor.
@@ -52,12 +52,12 @@ pub fn run(opts: Options) -> Result<()> {
width = opts.width,
height = opts.height,
fps = opts.fps,
- "M0 source: synthetic BGRx test pattern"
+ "spike source: synthetic BGRx test pattern"
);
Box::new(SyntheticCapturer::new(opts.width, opts.height, opts.fps))
}
Source::Portal => {
- tracing::info!("M0 source: xdg ScreenCast portal (live monitor)");
+ tracing::info!("spike source: xdg ScreenCast portal (live monitor)");
capture::open_portal_monitor().context("open portal capturer")?
}
Source::KwinVirtual => {
@@ -66,7 +66,7 @@ pub fn run(opts: Options) -> Result<()> {
width = opts.width,
height = opts.height,
?compositor,
- "M0 source: virtual output (PUNKTFUNK_COMPOSITOR)"
+ "spike source: virtual output (PUNKTFUNK_COMPOSITOR)"
);
let mut vd = crate::vdisplay::open(compositor).context("open virtual display")?;
let vout = vd
@@ -104,7 +104,7 @@ pub fn run(opts: Options) -> Result<()> {
opts.fps,
opts.bitrate_bps,
first.is_cuda(),
- 8, // m0 synthetic harness: 8-bit
+ 8, // spike synthetic harness: 8-bit
)
.context("open encoder")?;
@@ -147,7 +147,7 @@ pub fn run(opts: Options) -> Result<()> {
out = %opts.out.display(),
elapsed_s = format!("{elapsed:.2}"),
encode_fps = format!("{:.1}", stats.encoded as f64 / elapsed.max(1e-9)),
- "M0 capture→encode→file complete"
+ "spike capture→encode→file complete"
);
if let Some(lb) = lb {
@@ -194,7 +194,7 @@ fn drain_encoder(
/// A host↔client `punktfunk_core` pair over a lossless in-process loopback. Each encoded AU is
/// FEC-protected, packetized, sent, then reassembled on the client and byte-compared to the
-/// original — exercising the core on real encoder output (the M0 "feed into a Session" goal).
+/// original — exercising the core on real encoder output (the spike "feed into a Session" goal).
struct Loopback {
host: Session,
client: Session,
diff --git a/crates/punktfunk-host/src/vdisplay.rs b/crates/punktfunk-host/src/vdisplay.rs
index 95587744..169e268e 100644
--- a/crates/punktfunk-host/src/vdisplay.rs
+++ b/crates/punktfunk-host/src/vdisplay.rs
@@ -125,7 +125,7 @@ impl Compositor {
/// installed (it spawns a nested session — independent of the running desktop), plus the live
/// session's own compositor (KWin / Mutter / wlroots) when the host runs inside it. Cheap,
/// side-effect-free probes — safe to call per management request. A concrete client preference
-/// is validated against this set before it's honored (see the m3 handshake's resolution).
+/// is validated against this set before it's honored (see the punktfunk/1 handshake's resolution).
pub fn available() -> Vec {
#[cfg(target_os = "linux")]
{
diff --git a/crates/punktfunk-host/src/vdisplay/wlroots.rs b/crates/punktfunk-host/src/vdisplay/wlroots.rs
index 33d7488d..464cf345 100644
--- a/crates/punktfunk-host/src/vdisplay/wlroots.rs
+++ b/crates/punktfunk-host/src/vdisplay/wlroots.rs
@@ -40,7 +40,7 @@ fn chooser_file() -> String {
}
/// The managed xdpw config: per-session output selection with no GUI. The `|| echo` fallback
-/// keeps plain portal capture (`--source portal`, M0 flow) working when no session has written
+/// keeps plain portal capture (`--source portal` flow) working when no session has written
/// the chooser file. xdpw runs `chooser_cmd` via `/bin/sh -c`, reads stdout.
fn xdpw_config() -> String {
format!(
diff --git a/crates/punktfunk-host/src/wgc_helper.rs b/crates/punktfunk-host/src/wgc_helper.rs
index bc519bfd..7a08f991 100644
--- a/crates/punktfunk-host/src/wgc_helper.rs
+++ b/crates/punktfunk-host/src/wgc_helper.rs
@@ -50,7 +50,7 @@ pub fn run(opts: HelperOptions) -> Result<()> {
// path. Elevate its OS priority so a CPU-heavy game can't deschedule it and delay submission (which
// would leave our HIGH GPU priority with nothing queued to prioritise). Apollo's capture thread is
// likewise CRITICAL.
- crate::m3::boost_thread_priority(true);
+ crate::punktfunk1::boost_thread_priority(true);
// Capture the EXISTING SudoVDA output by GDI name / target id — do NOT create one (the host owns
// the virtual output + its isolate/restore; a second topology owner breaks DDA recovery).
diff --git a/docs-site/content/docs/apple-stage2-presenter.md b/docs-site/content/docs/apple-stage2-presenter.md
index 250e0d11..3d52db32 100644
--- a/docs-site/content/docs/apple-stage2-presenter.md
+++ b/docs-site/content/docs/apple-stage2-presenter.md
@@ -102,10 +102,10 @@ same-host-only, as today.
- `swift test`: add a decode-output test (decode a known IDR built like
`VideoToolboxRoundTripTests` → assert a `CVPixelBuffer` of the right dimensions + the
decode callback fires). Present is display-bound — validate it **live** via the HUD number.
-- Live: connect to a Linux host (`m3-host --source virtual` on the GNOME box; see
+- Live: connect to a Linux host (`punktfunk1-host --source virtual` on the GNOME box; see
[Ubuntu — GNOME](/docs/ubuntu-gnome)), confirm `capture→present` is a few ms over `capture→client`
and that `decode→present` shrank vs. an `AVSampleBufferDisplayLayer` baseline.
-- Compare against the headless reference number: `punktfunk-client-rs` reports skew-corrected
+- Compare against the headless reference number: `punktfunk-probe` reports skew-corrected
capture→reassembled (~1.3 ms p50 GNOME box → dev box); capture→present should be that **+ decode +
present**.
diff --git a/docs-site/content/docs/clients.md b/docs-site/content/docs/clients.md
index 34c2c663..1bdd8c1f 100644
--- a/docs-site/content/docs/clients.md
+++ b/docs-site/content/docs/clients.md
@@ -56,7 +56,7 @@ punktfunk-client --connect :9777 # skip the picker, start a session imme
## Windows desktop client (in development)
-`punktfunk-client` for Windows (`crates/punktfunk-client-windows`) is the native graphical client
+`punktfunk-client` for Windows (`clients/windows`) is the native graphical client
for Windows — pure Rust, the same `punktfunk/1` core as the Apple and Linux apps, with a **WinUI 3**
UI (host list, settings, PIN pairing) and the video on a `SwapChainPanel`, plus WASAPI audio, FFmpeg
decode, SDL3 controllers, network discovery, and PIN pairing. Launch it and pick a host from the
@@ -74,13 +74,13 @@ Until it ships, **Moonlight** remains the recommended way to stream to Windows (
## Linux reference client (headless)
-`punktfunk-client-rs` (in the repo) is a command-line client for the native protocol, used for
+`punktfunk-probe` (in the repo) is a command-line client for the native protocol, used for
testing, development, and latency measurement — not an everyday client. It connects, streams to a
file, runs the speed test, and can discover hosts:
```sh
-punktfunk-client-rs --discover # list hosts on the network
-punktfunk-client-rs --connect :9777 --pin # connect to one
+punktfunk-probe --discover # list hosts on the network
+punktfunk-probe --connect :9777 --pin # connect to one
```
## Which should I use?
@@ -90,6 +90,6 @@ punktfunk-client-rs --connect :9777 --pin # connect to one
| A Mac, iPhone, iPad, or Apple TV | The **Apple app** |
| A Linux desktop or laptop, or a Steam Deck | **`punktfunk-client`** (GTK4) |
| Windows, Android, a browser, a TV | **Moonlight** |
-| Automated tests / latency measurement | **`punktfunk-client-rs`** (headless) |
+| Automated tests / latency measurement | **`punktfunk-probe`** (headless) |
Whichever you choose, the first connection needs a one-time [pairing](/docs/pairing).
diff --git a/docs-site/content/docs/configuration.md b/docs-site/content/docs/configuration.md
index eb55ec45..59ab3800 100644
--- a/docs-site/content/docs/configuration.md
+++ b/docs-site/content/docs/configuration.md
@@ -47,7 +47,7 @@ Today the native `punktfunk/1` host (`serve --native`) streams **one session at
clients wait in the accept queue until the active session ends. Each session gets its own virtual
display at the client's exact resolution; concurrent native sessions are on the roadmap.
-(`m3-host`, the standalone test host, has a `--max-concurrent N` knob, default 4, bounded by your
+(`punktfunk1-host`, the standalone test host, has a `--max-concurrent N` knob, default 4, bounded by your
GPU's encoder — see the [Host CLI](/docs/host-cli) reference — but `serve --native` does **not** take
that flag.)
diff --git a/docs-site/content/docs/gamescope-multiuser.md b/docs-site/content/docs/gamescope-multiuser.md
index 23bdd5b8..da40efa0 100644
--- a/docs-site/content/docs/gamescope-multiuser.md
+++ b/docs-site/content/docs/gamescope-multiuser.md
@@ -29,7 +29,7 @@ Each gamescope **process is per-session** (`vdisplay/gamescope.rs::create()` spa
- **EIS input socket — single global file.** gamescope exports `LIBEI_SOCKET` for its children; a
shell wrapper relays it to the fixed path `/tmp/punktfunk-gamescope-ei` (`EI_SOCKET_FILE`).
**Two concurrent instances overwrite each other's socket name** in that one file.
-- **Injector — one host-lifetime `!Send` service.** `m3.rs::InjectorService` opens **one**
+- **Injector — one host-lifetime `!Send` service.** `punktfunk1.rs::InjectorService` opens **one**
`inject::open(backend)` for the whole run and forwards events over an mpsc channel. It was made
shared deliberately (the portal `CreateSession` churn wedged KWin's EIS — "EIS setup timed out").
For gamescope it reads the one global socket file, so all sessions' input lands in whichever
diff --git a/docs-site/content/docs/m2-plan.md b/docs-site/content/docs/gamestream-host-plan.md
similarity index 97%
rename from docs-site/content/docs/m2-plan.md
rename to docs-site/content/docs/gamestream-host-plan.md
index 86159373..c76a2885 100644
--- a/docs-site/content/docs/m2-plan.md
+++ b/docs-site/content/docs/gamestream-host-plan.md
@@ -1,5 +1,5 @@
---
-title: "M2 — Moonlight Host"
+title: "GameStream Host"
description: "Stream to a stock Moonlight client on a client-sized virtual display."
---
@@ -72,13 +72,13 @@ Ground-truth protocol reference: [`research/gamestream-protocol-research.json`](
handshake, negotiate `Config`, create a wlroots virtual output sized to the client.
*Acceptance: Moonlight completes RTSP and the host stands up the UDP streams.*
- **P1.3 — Video (punktfunk-core P1 codec), plaintext, clean-LAN.** RTP+NV framing + FEC shard
- layout in punktfunk-core; wire M0's NVENC AUs → UDP 47998. *Acceptance: Moonlight DISPLAYS video.*
+ layout in punktfunk-core; wire the spike's NVENC AUs → UDP 47998. *Acceptance: Moonlight DISPLAYS video.*
- **P1.4 — Control + input.** ENet (`rusty_enet`) control stream; decode input → `inject.rs`
(uinput/reis); request-IDR → force NVENC keyframe. *Acceptance: mouse/keyboard work.*
- **P1.5 — Robustness: FEC recovery + encryption.** nanors-exact FEC; per-shard AES-GCM.
*Acceptance: stable under `tc netem` loss; encrypted streams.*
- **P1.6 — Audio + polish.** Opus + audio RTP/FEC/CBC (UDP 47999); disconnect teardown; KWin
- backend for the user's KDE box. *Acceptance: full game stream with sound — the M2 goal.*
+ backend for the user's KDE box. *Acceptance: full game stream with sound — the GameStream-host goal.*
## Crates (verified available)
diff --git a/docs-site/content/docs/host-cli.md b/docs-site/content/docs/host-cli.md
index ce173f54..83f391e8 100644
--- a/docs-site/content/docs/host-cli.md
+++ b/docs-site/content/docs/host-cli.md
@@ -32,15 +32,15 @@ token is **required** when you bind the API off loopback with `--mgmt-bind`.
By default the host **requires pairing** — see [Pairing & Trust](/docs/pairing). On `serve --native` you
**arm pairing from the web console** (or mgmt API); the host then displays a 4-digit PIN. Pass `--open` to
turn off the mandatory-pairing default and serve any device on the network (trusted single-user setups
-only). The pairing flags below are `m3-host`-only and do **not** apply to `serve`.
+only). The pairing flags below are `punktfunk1-host`-only and do **not** apply to `serve`.
-## `m3-host`
+## `punktfunk1-host`
A standalone native-only host, mainly for testing the `punktfunk/1` path without the GameStream server
or web console.
```sh
-punktfunk-host m3-host --source virtual
+punktfunk-host punktfunk1-host --source virtual
```
| Flag | Meaning |
@@ -53,12 +53,12 @@ punktfunk-host m3-host --source virtual
| `--allow-pairing` | Accept PIN pairing; the host prints a PIN when a client pairs. |
| `--require-pairing` | Only serve paired devices (implies `--allow-pairing`). |
-`--max-concurrent`, `--allow-pairing`, and `--require-pairing` are **`m3-host`-only** — `serve` does not
+`--max-concurrent`, `--allow-pairing`, and `--require-pairing` are **`punktfunk1-host`-only** — `serve` does not
accept them. On `serve --native` you arm pairing from the web console instead, and concurrency is not
yet capped from the command line.
-Both `serve --native` and `m3-host` advertise the host on the network so clients can discover it. List
-hosts from another machine with `punktfunk-client-rs --discover`.
+Both `serve --native` and `punktfunk1-host` advertise the host on the network so clients can discover it. List
+hosts from another machine with `punktfunk-probe --discover`.
## Environment
diff --git a/docs-site/content/docs/implementation-plan.md b/docs-site/content/docs/implementation-plan.md
index b3885ce2..64526a84 100644
--- a/docs-site/content/docs/implementation-plan.md
+++ b/docs-site/content/docs/implementation-plan.md
@@ -276,7 +276,7 @@ punktfunk/
│ │ ├── src/vdisplay/ # trait + kwin/wlroots/mutter impls
│ │ ├── src/input/ # reis + uinput
│ │ └── src/web/ # axum config/pairing API
-│ └── punktfunk-client-rs/ # reference Rust client (M4)
+│ └── punktfunk-probe/ # reference Rust client (M4)
├── clients/
│ ├── apple/ # Swift package, imports punktfunk_core.h (M5)
│ └── android/ # Kotlin + JNI (later)
diff --git a/docs-site/content/docs/pairing.md b/docs-site/content/docs/pairing.md
index f3343e66..97d975df 100644
--- a/docs-site/content/docs/pairing.md
+++ b/docs-site/content/docs/pairing.md
@@ -45,7 +45,7 @@ host's management console, click to arm pairing, and the host displays a 4-digit
list of paired devices. This works on a headless host over the network — there is no command-line flag
to arm pairing on `serve`.
-(The standalone headless test host, `m3-host`, takes `--allow-pairing`/`--require-pairing` on its
+(The standalone headless test host, `punktfunk1-host`, takes `--allow-pairing`/`--require-pairing` on its
command line instead; the production `serve --native` host arms pairing from the console.)
Then, on the client:
@@ -60,13 +60,13 @@ the right setting on a shared network: a device has to complete the PIN ceremony
connect.
If you're on a fully trusted single-user network and want to skip pairing, run the host open with
-`serve --native --open` (or `m3-host --allow-tofu` for the standalone host) — it then advertises
+`serve --native --open` (or `punktfunk1-host --allow-tofu` for the standalone host) — it then advertises
`pair=optional` and accepts unpaired clients. Requiring pairing is strongly recommended.
## Trust-on-first-use (host opt-in)
Trust-on-first-use (TOFU) is **off by default** and is an explicit *host* opt-in for fully trusted
-networks. A host enables it by running open — `m3-host --allow-tofu` or `serve --open` — which makes
+networks. A host enables it by running open — `punktfunk1-host --allow-tofu` or `serve --open` — which makes
it advertise `pair=optional` over mDNS and accept unpaired clients. Only then does a client offer the
TOFU path: connecting to such a host for the first time shows the host's fingerprint and asks you to
confirm it (compare it with the one the host logged at startup), then pins it. The client presents
diff --git a/docs-site/content/docs/roadmap.md b/docs-site/content/docs/roadmap.md
index 5d4081c2..1fa064d6 100644
--- a/docs-site/content/docs/roadmap.md
+++ b/docs-site/content/docs/roadmap.md
@@ -20,7 +20,7 @@ Steam session at the **client's exact resolution + refresh** — games see it (v
change, reused (no Steam restart) on the same mode. Plus macOS/iPad input fixes (NSEvent motion +
iPad pointer-lock) and a 4K/5K one-frame-freeze fix (grow the UDP socket buffers).
-**Next:** **§8 pairing & trust hardening** (mandatory PIN by default + delegated approval), the M4
+**Next:** **§8 pairing & trust hardening** (mandatory PIN by default + delegated approval), the native
client presenter + iOS (§6), and a Windows host (§7 — now **de-risked via SudoVDA**, no custom
signed driver needed). **§10 HDR/10-bit is parked — blocked upstream at the compositor** (no
gamescope/KWin PipeWire 10-bit producer yet).
@@ -88,7 +88,7 @@ select = a `pw_stream` with `Direction::Output` + `media.class=Audio/Source`.
- **Touch — implemented (host path), pending a backend that lands it.** `TouchDown/Move/Up`
InputKinds (reuse the abs-pointer `flags=(w<<16)|h` mapping, `code`=touch id); host
`inject/libei.rs` requests the `Touchscreen` device type + binds the `Touch` capability and
- injects `ei_touchscreen` down/motion/up; `punktfunk-client-rs --touch-test` drags a finger.
+ injects `ei_touchscreen` down/motion/up; `punktfunk-probe --touch-test` drags a finger.
**Validated:** KWin's RemoteDesktop portal *grants* the Touchscreen device type, but its EIS
server creates **no touchscreen device** (headless KWin) — so touch currently no-ops on KWin
(now logged once). The code is correct; it needs a backend that exposes `ei_touchscreen`
@@ -102,14 +102,14 @@ select = a `pw_stream` with `Direction::Output` + `media.class=Audio/Source`.
trigger effects (L2/R2)**. Protocol carries new side-planes: rich-input `0xCC`
(touchpad/motion) + HID-output `0xCD` (LED/triggers). `/dev/uhid` udev rule shipped.
- **Rich DualSense — Phase C/D/E end-to-end, validated live.** `PUNKTFUNK_GAMEPAD=dualsense`
- selects a per-session `DualSenseManager` (the `PadBackend` enum in `m3.rs`): client gamepad frames
+ selects a per-session `DualSenseManager` (the `PadBackend` enum in `punktfunk1.rs`): client gamepad frames
build the DualSense report; the kernel's feedback comes back as `HidOutput` on the **0xCD** plane
(lightbar / player LEDs / adaptive triggers) while **rumble stays on the universal 0xCA plane**
(so non-DualSense clients still feel it); touchpad + motion ride the **0xCC** rich-input plane
(`DualSenseManager::apply_rich`, merged with button state). The connector + C ABI gained
`punktfunk_connection_next_hidout` (→ `PunktfunkHidOutput`) and `punktfunk_connection_send_rich_input`
- (← `PunktfunkRichInput`); header regenerated. Validated on-box: a synthetic-source `m3-host` +
- `punktfunk-client-rs --rich-input-test` created the real kernel DualSense, drove 0xCC, and decoded
+ (← `PunktfunkRichInput`); header regenerated. Validated on-box: a synthetic-source `punktfunk1-host` +
+ `punktfunk-probe --rich-input-test` created the real kernel DualSense, drove 0xCC, and decoded
12 live 0xCD events (the kernel's actual lightbar/trigger init reports) — data plane unaffected
(600/600 frames). *Remaining:* the Apple client renders adaptive triggers + rumble on a real
DualSense (`GCDualSenseAdaptiveTrigger`) — handed off to the client agent for the real playtest.
@@ -192,7 +192,7 @@ value) instead of guesswork that ends in a stuttering stream.
and exposes it (`punktfunk_connection_speed_test()` + `punktfunk_connection_probe_result()` →
`PunktfunkProbeResult{throughput_kbps, loss_pct, …}`). Probe filler is diverted from the decoder.
Validated on loopback (synthetic source): a 20 Mbps/2 s probe measured 20050 kbps at 0% loss,
- interleaved probe AUs excluded from frame verification. `punktfunk-client-rs` gains `--bitrate` +
+ interleaved probe AUs excluded from frame verification. `punktfunk-probe` gains `--bitrate` +
`--speed-test KBPS:MS` as the reference/loopback driver.
**Done (Apple client UI):** Settings grows a Bitrate control (Automatic = host default; manual is
@@ -244,7 +244,7 @@ the GF(2⁸)/Moonlight ~1 Gbps wall). A 6-way subagent investigation (2026-06-11
**Verdict: ~halfway, and it's mostly clamps + ONE real piece of work.** Already 1 Gbps-ready and
untouched: the integer/type path (u32 kbps → u64 → int64_t, no truncation); FEC (a 1 Gbps frame is
only ~434–874 data shards = a single GF(2¹⁶) block, two orders under the 65535 ceiling); AES-GCM
-(RustCrypto auto AES-NI, ~10–25× headroom on x86_64); the u64 sequence/nonce space; and the **M1
+(RustCrypto auto AES-NI, ~10–25× headroom on x86_64); the u64 sequence/nonce space; and the **core
`ReassemblerLimits`** — fully *derived* from the negotiated `FecConfig`, so they already admit every
legit high-bitrate frame with nothing to relax. Security invariant to keep: every allocation size
must trace to a host-negotiated parameter clamped to a scheme ceiling — scale via the negotiated
@@ -271,7 +271,7 @@ params (`max_data_per_block`, `shard_payload`), never by widening a bound by han
- **DoS hygiene (last):** derive the one hardcoded reassembler field (`max_frame_bytes` = 64 MiB,
never set by `session_config`) from the negotiated mode/bitrate — strictly *tightens* the surface.
- **Validate with the speed-test probe** (it reuses the real `submit_frame`→FEC+crypto+send path):
- `punktfunk-client-rs --speed-test KBPS:MS`, RELEASE build (debug is CPU-bound ~30 Mbps), watching
+ `punktfunk-probe --speed-test KBPS:MS`, RELEASE build (debug is CPU-bound ~30 Mbps), watching
`packets_send_dropped`. Open Qs: NVENC CBR rate-tracking at 0.5–1 Gbps (no explicit
`rc_buffer_size`); LAN/QEMU-NIC jumbo/GSO support; any `web/` bitrate slider hardcoding 500 Mbps.
@@ -344,7 +344,7 @@ buffer; `sendmmsg`/`recvmmsg` batching; the capture-timestamp anchor placement.
The native protocol had no discovery — clients connected by `--connect HOST:PORT` only, while
GameStream already auto-discovered via mDNS (`_nvstream._tcp`). Now both the unified host
-(`serve --native`) and standalone `m3-host` advertise the native service over mDNS:
+(`serve --native`) and standalone `punktfunk1-host` advertise the native service over mDNS:
- **Service**: `_punktfunk._udp.local.` (UDP — punktfunk/1 is QUIC; the advertised port is the QUIC
control/data port). Host side: `crate::discovery::advertise_native`, wired into `m3::serve` so
@@ -353,7 +353,7 @@ GameStream already auto-discovered via mDNS (`_nvstream._tcp`). Now both the uni
- **TXT records**: `proto=punktfunk/1`, `fp=` (the value a client pins — advisory
over unauthenticated mDNS, TOFU/pinning still verifies on connect), `pair=required|optional`
(so a picker knows up front whether the PIN ceremony is needed), `id=` (dedup).
-- **Client**: `punktfunk-client-rs --discover [SECS]` browses and prints each host (name, addr:port,
+- **Client**: `punktfunk-probe --discover [SECS]` browses and prints each host (name, addr:port,
pairing, fingerprint), then exits. Apple clients browse the same service natively via NWBrowser
(Bonjour) — no Rust-connector dependency; this section's service type + TXT keys are the contract.
- **Validated**: cross-LAN — dev box discovered the GNOME-box appliance
diff --git a/docs-site/content/docs/running-as-a-service.md b/docs-site/content/docs/running-as-a-service.md
index 48a69bfa..de31238e 100644
--- a/docs-site/content/docs/running-as-a-service.md
+++ b/docs-site/content/docs/running-as-a-service.md
@@ -82,7 +82,7 @@ session unit — see [Bazzite](/docs/bazzite).
After a reboot, from another machine on the network:
```sh
-punktfunk-client-rs --discover # or just look for the host in the Apple app / Moonlight
+punktfunk-probe --discover # or just look for the host in the Apple app / Moonlight
```
If the host is listed, it's up. If not, check `journalctl --user -u punktfunk-host` on the host.
diff --git a/docs-site/content/docs/status.md b/docs-site/content/docs/status.md
index 73ff23de..9f2cb05f 100644
--- a/docs-site/content/docs/status.md
+++ b/docs-site/content/docs/status.md
@@ -11,10 +11,10 @@ and the design in the [Implementation Plan](/docs/implementation-plan); this pag
| Milestone | State |
|---|---|
-| **M1** — `punktfunk-core` + C ABI (protocol · FEC · crypto) | ✅ complete & hardened |
-| **M2** — GameStream host (Moonlight-compatible) | ✅ working end-to-end; HDR/surround-audio polish open |
-| **M3** — `punktfunk/1` native protocol (QUIC control + UDP data) | ✅ full session planes, validated live |
-| **M4** — native client decode + present (Apple first) | 🟡 macOS stage 1 live; stage-2 presenter built + decode-tested (opt-in, present needs live validation). **Linux GTK client stage 1 live** (2026-06-12) |
+| **Core** — `punktfunk-core` + C ABI (protocol · FEC · crypto) | ✅ complete & hardened |
+| **GameStream host** (Moonlight-compatible) | ✅ working end-to-end; HDR/surround-audio polish open |
+| **Native protocol** — `punktfunk/1` (QUIC control + UDP data) | ✅ full session planes, validated live |
+| **Native clients** — decode + present (Apple first) | 🟡 macOS stage 1 live; stage-2 presenter built + decode-tested (opt-in, present needs live validation). **Linux GTK client stage 1 live** (2026-06-12) |
## Live on the boxes
@@ -29,7 +29,7 @@ All three appliances advertise over mDNS (`_punktfunk._udp`) and require PIN pai
## Progress log
### 2026-06-12
-- **Native Linux client — stage 1, first light** (`crates/punktfunk-client-linux`, binary
+- **Native Linux client — stage 1, first light** (`clients/linux`, binary
`punktfunk-client`). GTK4/libadwaita app on the **Option A** architecture picked after a
six-angle research pass (toolkits / hw decode / Wayland presentation / input capture /
prior art / codebase): links `punktfunk-core` directly as a crate (no C ABI;
@@ -81,7 +81,7 @@ All three appliances advertise over mDNS (`_punktfunk._udp`) and require PIN pai
client's capture→reassembled latency valid **cross-machine**. Validated GNOME box → dev box:
offset −1.57 ms removed, **p50 1.30 ms** skew-corrected. (`05bc9ab`)
- **Native LAN auto-discovery** — host advertises `_punktfunk._udp` (TXT: fingerprint, pairing,
- proto); `punktfunk-client-rs --discover` lists hosts. Validated cross-LAN. (`4fff464`)
+ proto); `punktfunk-probe --discover` lists hosts. Validated cross-LAN. (`4fff464`)
- **Third test box stood up** — home-worker-3 (Ubuntu 26.04, RTX 4090, GNOME 50): first GNOME/Mutter
zero-copy streaming on a real desktop; **1 Gbps probe clean** (625 MB/5 s, `send_dropped=0`).
Two physical-NVIDIA gotchas documented in [Ubuntu — GNOME](/docs/ubuntu-gnome).
diff --git a/docs-site/content/docs/windows-host.md b/docs-site/content/docs/windows-host.md
index 6fbabf26..47e87a8e 100644
--- a/docs-site/content/docs/windows-host.md
+++ b/docs-site/content/docs/windows-host.md
@@ -25,7 +25,7 @@ punktfunk is cleanly layered. **~95% of the codebase is platform-agnostic and re
| QUIC control plane (`quic.rs`, pairing, mode negotiation) | quinn + tokio are portable |
| GameStream P1.1 (mDNS, serverinfo, pairing, RTSP, ENet) — *except* `stream.rs`/`audio.rs` | pure wire logic |
| Management REST API (`mgmt.rs`) + OpenAPI | axum/tokio, portable |
-| Pipeline + `m3.rs` orchestration | trait-generic — calls `capturer.next_frame()`, `encoder.submit/poll()`; **needs zero changes** |
+| Pipeline + `punktfunk1.rs` orchestration | trait-generic — calls `capturer.next_frame()`, `encoder.submit/poll()`; **needs zero changes** |
| The **trait boundaries** themselves: `Capturer`, `Encoder`, `VirtualDisplay`, `InputInjector`, `AudioCapturer`, `VirtualMic` | platform-neutral signatures; Linux deps are already isolated under `[target.'cfg(target_os="linux")'.dependencies]` |
So a Windows host is **new `#[cfg(target_os = "windows")]` backend modules behind the existing
diff --git a/include/punktfunk_core.h b/include/punktfunk_core.h
index 1539b02d..e4d0e417 100644
--- a/include/punktfunk_core.h
+++ b/include/punktfunk_core.h
@@ -909,6 +909,19 @@ PunktfunkStatus punktfunk_connection_request_mode(const PunktfunkConnection *c,
PunktfunkStatus punktfunk_connection_request_keyframe(const PunktfunkConnection *c);
#endif
+#if defined(PUNKTFUNK_FEATURE_QUIC)
+// Cumulative access units the host→client reassembler dropped as unrecoverable (FEC couldn't
+// rebuild them). A video loop polls this and calls [`punktfunk_connection_request_keyframe`]
+// when it climbs — the correct loss trigger under the host's infinite GOP, where unrecoverable
+// loss yields reference-missing delta frames the decoder *silently conceals* (frozen / garbage
+// picture, no decode error), so a decode-error trigger rarely fires. Monotonic for the session;
+// compare against the last observed value. Writes 0 to `out` on a NULL connection.
+//
+// # Safety
+// `c` is a valid connection handle; `out` is writable (NULL is skipped).
+PunktfunkStatus punktfunk_connection_frames_dropped(const PunktfunkConnection *c, uint64_t *out);
+#endif
+
#if defined(PUNKTFUNK_FEATURE_QUIC)
// Start a bandwidth speed test: ask the host to burst filler over the data plane at
// `target_kbps` of goodput for `duration_ms` (each clamped host-side to ≤ 3 Gbps / ≤ 5 s),
diff --git a/packaging/bazzite/README.md b/packaging/bazzite/README.md
index 5ed00a0d..1627582f 100644
--- a/packaging/bazzite/README.md
+++ b/packaging/bazzite/README.md
@@ -236,9 +236,9 @@ journalctl --user -u punktfunk-host -f
> **What `serve` actually starts.** The unit's `ExecStart` runs `punktfunk-host serve`, which is the
> **GameStream / Moonlight-compatible** host (mDNS discovery, pairing, RTSP, the fixed GameStream
> ports, **plus the management REST API on 47990**). The native `punktfunk/1` (QUIC) host is a
-> *separate* subcommand — `punktfunk-host m3-host` — and is **not** what the bundled systemd unit
+> *separate* subcommand — `punktfunk-host punktfunk1-host` — and is **not** what the bundled systemd unit
> launches. So out of the box on Bazzite you get the **Moonlight-compatible** host.
-> (Source: `crates/punktfunk-host/src/main.rs` — `serve` → `gamestream::serve`; `m3-host` is its own
+> (Source: `crates/punktfunk-host/src/main.rs` — `serve` → `gamestream::serve`; `punktfunk1-host` is its own
> path.)
> **Unit caveat:** `scripts/punktfunk-host.service` declares only `After=pipewire.service` and (in
@@ -253,7 +253,7 @@ journalctl --user -u punktfunk-host -f
> ⚠️ **There is no firewall script or firewall doc in the repo.** The ports below are derived
> directly from the code constants (`crates/punktfunk-host/src/gamestream/mod.rs`, `mgmt.rs`) and
-> the M2 port-map (`docs/m2-plan.md`). Treat the `firewall-cmd` lines as recommended-but-verified,
+> the GameStream-host port-map (`docs/gamestream-host-plan.md`). Treat the `firewall-cmd` lines as recommended-but-verified,
> not a checked-in script.
**GameStream / Moonlight ports** (fixed; Moonlight derives them from the HTTP base):
@@ -285,16 +285,16 @@ sudo firewall-cmd --permanent --add-port=47998/udp \
sudo firewall-cmd --reload
```
-**If you also run the native `punktfunk/1` host** (`punktfunk-host m3-host`, not started by the
+**If you also run the native `punktfunk/1` host** (`punktfunk-host punktfunk1-host`, not started by the
default unit):
- **QUIC control plane: UDP 9777** (default `--port`; change with `--port N`).
-- **Data plane: an *ephemeral* UDP port** — `m3-host` binds `0.0.0.0:0` and tells the client which
+- **Data plane: an *ephemeral* UDP port** — `punktfunk1-host` binds `0.0.0.0:0` and tells the client which
port it got, so there is **no fixed data port to open**. For a restrictive firewall you'd need to
allow the ephemeral UDP range; the repo does not pin one.
```sh
-# Only if you run `m3-host`:
+# Only if you run `punktfunk1-host`:
sudo firewall-cmd --permanent --add-port=9777/udp && sudo firewall-cmd --reload
```
@@ -341,8 +341,8 @@ advertising`, and an RTSP listening line on port 48010. No NVENC/EGL errors on t
- Launch the app — you should get video at your client's native resolution/refresh, with the nested
`steam -gamepadui` (or whatever `PUNKTFUNK_GAMESCOPE_APP` you set) running inside gamescope.
-**3. (Optional) native punktfunk/1 client** — only if you're running the separate `m3-host`. The
-repo's reference client is `punktfunk-client-rs`, e.g. `punktfunk-client-rs --mode 1280x720x120 --out
+**3. (Optional) native punktfunk/1 client** — only if you're running the separate `punktfunk1-host`. The
+repo's reference client is `punktfunk-probe`, e.g. `punktfunk-probe --mode 1280x720x120 --out
/tmp/a.h265` (add `--pin HEX` for PIN pairing). This is a headless/decode-to-file reference, not a
desktop viewer.
@@ -414,5 +414,5 @@ matching your Bazzite Fedora base (`rpm -E %fedora`).
1. The COPR is **operator-run / not assumed published** — both install paths depend on it.
2. There is **no firewall script/doc in the repo** — the ports above are derived from the code.
3. The bundled systemd unit runs the **GameStream/Moonlight** `serve` host, **not** the native
- `punktfunk/1` QUIC host (`m3-host` is separate and unmanaged by the unit).
+ `punktfunk/1` QUIC host (`punktfunk1-host` is separate and unmanaged by the unit).
4. The mgmt port (47990) is **loopback-only by default** — don't open it.
diff --git a/packaging/debian/README.md b/packaging/debian/README.md
index 2b86f301..21c064a8 100644
--- a/packaging/debian/README.md
+++ b/packaging/debian/README.md
@@ -10,7 +10,7 @@ The same workflow also publishes **`punktfunk-web`** (the browser management con
status) and **`punktfunk-client`** (the GTK4 couch/Deck client). `punktfunk-host` **Recommends**
`punktfunk-web`, so a default `apt install punktfunk-host` pulls the console too (alongside the
udev/sysctl bits) unless you've disabled weak deps; `punktfunk-client` is independent — install it
-on the box you stream *to*. (`punktfunk-client-rs` is the headless reference/test tool, not packaged
+on the box you stream *to*. (`punktfunk-probe` is the headless reference/test tool, not packaged
here.)
Package layout mirrors the Fedora RPM (`../rpm/punktfunk.spec`): the host binary, the `/dev/uinput`
diff --git a/packaging/flatpak/io.unom.Punktfunk.yml b/packaging/flatpak/io.unom.Punktfunk.yml
index da5ae855..2eb3e9ff 100644
--- a/packaging/flatpak/io.unom.Punktfunk.yml
+++ b/packaging/flatpak/io.unom.Punktfunk.yml
@@ -71,7 +71,7 @@ finish-args:
- --socket=pulseaudio
# --- network: QUIC control + UDP data plane + mDNS discovery (_punktfunk._udp) ---
- --share=network
- # --- persistent client identity / pairing store (shared with punktfunk-client-rs) ---
+ # --- persistent client identity / pairing store (shared with punktfunk-probe) ---
- --filesystem=~/.config/punktfunk:create # client-{cert,key}.pem, known-hosts, settings
build-options:
@@ -145,7 +145,7 @@ modules:
# Windows client; this edits only the sandbox copy of Cargo.toml. (No --locked: the lock
# no longer matches the reduced member set; --offline still pins every crate to the
# vendored cargo-sources.json, so the build stays reproducible.)
- - sed -i '\#"crates/punktfunk-client-windows",#d' Cargo.toml
+ - sed -i '\#"clients/windows",#d' Cargo.toml
- cargo --offline build --release -p punktfunk-client-linux
- install -Dm0755 target/release/punktfunk-client ${FLATPAK_DEST}/bin/punktfunk-client
# Desktop entry (renamed to the app id; Exec is the in-sandbox binary).
diff --git a/scripts/bench/gpu-stream.sh b/scripts/bench/gpu-stream.sh
index b56ef70b..ec5c3c9b 100755
--- a/scripts/bench/gpu-stream.sh
+++ b/scripts/bench/gpu-stream.sh
@@ -35,18 +35,18 @@ if [[ ! -S "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ]]; then
fi
echo "==> building host + client (release)"
-cargo build -rq -p punktfunk-host -p punktfunk-client-rs
+cargo build -rq -p punktfunk-host -p punktfunk-probe
HOST_LOG="$(mktemp)"; CLI_LOG="$(mktemp)"
trap 'kill "$HOST_PID" 2>/dev/null; [[ -n "$OWN_KWIN" ]] && pkill -f "kwin_wayland --virtual" 2>/dev/null; rm -f "$HOST_LOG" "$CLI_LOG"' EXIT
-echo "==> host: m3-host --source virtual ($MODE, ${SECS}s)"
-target/release/punktfunk-host m3-host --source virtual --seconds "$SECS" --max-sessions 1 \
+echo "==> host: punktfunk1-host --source virtual ($MODE, ${SECS}s)"
+target/release/punktfunk-host punktfunk1-host --source virtual --seconds "$SECS" --max-sessions 1 \
>"$HOST_LOG" 2>&1 &
HOST_PID=$!
sleep 3
echo "==> client: streaming + measuring latency"
-target/release/punktfunk-client-rs --connect 127.0.0.1:9777 --mode "$MODE" --out /dev/null \
+target/release/punktfunk-probe --connect 127.0.0.1:9777 --mode "$MODE" --out /dev/null \
>"$CLI_LOG" 2>&1 || true
wait "$HOST_PID" 2>/dev/null || true
diff --git a/scripts/headless/run-headless-kde.sh b/scripts/headless/run-headless-kde.sh
index b8ef8c79..63dcfc22 100755
--- a/scripts/headless/run-headless-kde.sh
+++ b/scripts/headless/run-headless-kde.sh
@@ -14,7 +14,7 @@
#
# Then in another shell:
# WAYLAND_DISPLAY=wayland-kde XDG_CURRENT_DESKTOP=KDE PUNKTFUNK_ZEROCOPY=1 \
-# punktfunk-host m3-host --source virtual --seconds 14400
+# punktfunk-host punktfunk1-host --source virtual --seconds 14400
set -euo pipefail
RES="${1:-1920x1080}"
diff --git a/tools/latency-probe/src/main.rs b/tools/latency-probe/src/main.rs
index d9e38c1a..532b2dc9 100644
--- a/tools/latency-probe/src/main.rs
+++ b/tools/latency-probe/src/main.rs
@@ -1,4 +1,4 @@
-//! `latency-probe` — glass-to-glass latency measurement (plan §10, M3).
+//! `latency-probe` — glass-to-glass latency measurement (plan §10).
//!
//! Renders a timestamp/QR on the host, reads it back on the client capture (or a
//! photodiode for true photons), and tracks p50/p99. Build this before optimizing
@@ -7,5 +7,7 @@
//! Status: scaffold.
fn main() {
- println!("latency-probe: scaffold (M3 — render timestamp/QR on host, read back on client; track p50/p99)");
+ println!(
+ "latency-probe: scaffold (render timestamp/QR on host, read back on client; track p50/p99)"
+ );
}
diff --git a/tools/loss-harness/src/main.rs b/tools/loss-harness/src/main.rs
index d46ffcb9..1390e7f2 100644
--- a/tools/loss-harness/src/main.rs
+++ b/tools/loss-harness/src/main.rs
@@ -2,7 +2,7 @@
//!
//! Drives access units through the in-process loopback at increasing loss rates, for
//! both FEC schemes, and prints how many frames survive. A pure-software stand-in for
-//! `tc netem` that needs no network and runs anywhere `punktfunk_core` builds. The real M3
+//! `tc netem` that needs no network and runs anywhere `punktfunk_core` builds. The real punktfunk/1
//! harness adds `tc netem` jitter/reorder on the UDP path.
use punktfunk_core::config::{Config, FecConfig, FecScheme, ProtocolPhase, Role};