merge: display-management (Stages 0-5 §6A + keep-alive hardening + gaming-rig)
apple / swift (push) Successful in 1m15s
windows-host / package (push) Failing after 3m56s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m18s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m20s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 57s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m2s
release / apple (push) Successful in 8m43s
android / android (push) Successful in 4m17s
ci / rust (push) Failing after 29s
ci / web (push) Successful in 49s
arch / build-publish (push) Successful in 5m33s
ci / docs-site (push) Successful in 59s
apple / screenshots (push) Successful in 5m42s
deb / build-publish (push) Successful in 3m7s
decky / build-publish (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 28s
ci / bench (push) Successful in 4m41s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 44s
flatpak / build-publish (push) Successful in 4m30s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 10m21s
docker / deploy-docs (push) Successful in 20s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 9m25s

Merges display-mgmt-stage0 — the user-configurable virtual-display policy layer above the
per-compositor backends. On-glass validated (KWin .116 + Mutter .21; Windows compile-verified .173):

- Policy surface (keep_alive · topology · conflict · identity · layout · max) →
  display-settings.json, console-editable via /api/v1/display/{settings,state,release,layout} + a
  dedicated "Virtual displays" console section. All five axes enforced, not just stored.
- Lifecycle: pure state machine + Linux keep-alive pool (registry + DisplayLease ownership split),
  incl. keep_alive=forever/Pinned (freed via /display/release); topology extend/primary/exclusive
  (group-aware); per-client identity (KWin per-slot names → KDE scaling round-trips); mode_conflict
  admission (Windows default reject, single-capturer IDD); §6A multi-monitor (display groups +
  layout engine + console arrangement table — several clients as monitors of one desktop).
- Keep-alive reconnect hardened: same-client zombie preempt (never a 2nd display), deliberate-quit
  skip-linger (QUIT_CLOSE_CODE), tunable idle timeout (PUNKTFUNK_IDLE_TIMEOUT_MS).

Conflicts (packaging/{arch,debian}/README.md firewall docs): kept main's ufw/nft port commands +
the branch's --data-port documentation. build + clippy -D warnings + cargo test --workspace
(18 suites, 0 failed) green on the merged tree.
This commit is contained in:
2026-07-05 18:22:17 +00:00
43 changed files with 7161 additions and 483 deletions
+26 -2
View File
@@ -73,6 +73,14 @@ struct Args {
/// `--rich-input-test` — drive the DualSense touchpad + motion over 0xCC (host needs
/// `PUNKTFUNK_GAMEPAD=dualsense`); also logs the 0xCD HID-output feedback that comes back.
rich_input_test: bool,
/// `--quit` — close the connection with the deliberate-quit code (`QUIT_CLOSE_CODE`) at end of
/// stream, so the host tears its virtual display down immediately (skips keep-alive linger). A
/// bare exit closes with code 0 → the host lingers for a reconnect. Tests the #2 quit path.
quit: bool,
/// `--seconds N` — cap the receive loop at N seconds, then end the session gracefully (reach the
/// `conn.close`). Without it the loop runs to the 120s cap. Lets a test bound a live-host stream so
/// the client-initiated close (with/without `--quit`) fires promptly.
seconds: Option<u64>,
pin: Option<[u8; 32]>,
/// `--remode WxHxFPS:SECS` — request this mode SECS seconds into the stream.
remode: Option<(Mode, u32)>,
@@ -211,6 +219,8 @@ fn parse_args() -> Args {
mic_burst: argv.iter().any(|a| a == "--mic-burst"),
touch_test: argv.iter().any(|a| a == "--touch-test"),
rich_input_test: argv.iter().any(|a| a == "--rich-input-test"),
quit: argv.iter().any(|a| a == "--quit"),
seconds: get("--seconds").and_then(|s| s.parse().ok()),
pin,
remode,
pair: get("--pair").map(String::from),
@@ -1041,6 +1051,9 @@ async fn session(args: Args) -> Result<()> {
let mut net_us_v: Vec<u64> = Vec::new();
let mut last_rx = std::time::Instant::now();
let started = std::time::Instant::now();
// Stream-duration cap: `--seconds N`, else the 120s default. Ending the loop here reaches the
// graceful `conn.close` below (with the deliberate-quit code if `--quit`).
let cap_secs = args.seconds.unwrap_or(120);
// Adaptive-FEC loss window: publish a fresh estimate every 750 ms for the LossReport task.
let mut last_loss_report = std::time::Instant::now();
let (mut last_recovered, mut last_received, mut last_dropped) = (0u64, 0u64, 0u64);
@@ -1076,7 +1089,7 @@ async fn session(args: Args) -> Result<()> {
{
break;
}
if started.elapsed() > std::time::Duration::from_secs(120)
if started.elapsed() > std::time::Duration::from_secs(cap_secs)
|| last_rx.elapsed() > std::time::Duration::from_secs(8)
{
break;
@@ -1208,7 +1221,18 @@ async fn session(args: Args) -> Result<()> {
}
}
conn.close(0u32.into(), b"done");
// `--quit` closes with the deliberate-quit code so the host skips the keep-alive linger; a normal
// exit uses code 0 (an unwanted-disconnect close → the host lingers for a reconnect).
let close_code = if args.quit {
punktfunk_core::quic::QUIT_CLOSE_CODE
} else {
0
};
conn.close(close_code.into(), b"done");
// Flush the CONNECTION_CLOSE frame before we exit: without this the process can drop the endpoint
// before quinn sends the close, so the host waits out the idle timeout instead of seeing the close
// CODE promptly (deliberate-quit vs. code 0). Bounded so a stuck flush can't hang the probe.
let _ = tokio::time::timeout(std::time::Duration::from_secs(2), ep.wait_idle()).await;
result
}