Rust edition 2024: the whole tree, with the env-mutation class made visible (WP20) #177

Merged
enricobuehler merged 2 commits from worktree-edition-2024 into main 2026-08-12 15:38:25 +00:00
70 changed files with 803 additions and 638 deletions
+11 -1
View File
@@ -187,7 +187,17 @@ jobs:
- name: Verify generated header is committed & up to date
run: |
cargo build -p punktfunk-core --locked
cargo build -p punktfunk-core --locked >/tmp/core-build.log 2>&1 \
|| { cat /tmp/core-build.log; exit 1; }
cat /tmp/core-build.log
# build.rs demotes a cbindgen failure to a warning and then writes NOTHING — the
# checked-in header stays untouched and the drift check below stays green while the
# header is silently stale. So first assert the regeneration actually happened.
# (cargo replays build-script warnings from cache, so this holds on cached builds too.)
grep -q "punktfunk-core: wrote" /tmp/core-build.log
if grep -q "cbindgen failed" /tmp/core-build.log; then
echo "cbindgen failed to parse the ABI surface — header NOT regenerated" && exit 1
fi
git config --global --add safe.directory "$PWD"
git diff --exit-code include/punktfunk_core.h \
|| (echo "include/punktfunk_core.h is stale — commit the regenerated header" && exit 1)
+2 -2
View File
@@ -66,8 +66,8 @@ ndk = { path = "clients/android/native/vendor/ndk" }
[workspace.package]
version = "0.27.0"
edition = "2021"
rust-version = "1.82"
edition = "2024"
rust-version = "1.85"
license = "MIT OR Apache-2.0"
authors = ["unom"]
repository = "https://git.unom.io/unom/punktfunk"
+3 -3
View File
@@ -200,7 +200,7 @@ fn resolve(info: &ResolvedService) -> Option<Host> {
/// hold the Wi-Fi `MulticastLock` for the browse lifetime.
///
/// [`nativeDiscoveryStop`]: Java_io_unom_punktfunk_kit_NativeBridge_nativeDiscoveryStop
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeDiscoveryStart(
_env: JNIEnv,
_this: JObject,
@@ -214,7 +214,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeDiscoverySt
/// `NativeBridge.nativeDiscoveryPoll(handle): String` — the current resolved-host snapshot,
/// newline-joined records of `key␟name␟addr␟port␟fp␟pair␟mac␟os` (`␟` = U+001F). Empty string = no hosts /
/// `0` handle. Poll ~1 Hz from the UI thread (cheap: a mutex lock + string build).
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeDiscoveryPoll<'local>(
env: JNIEnv<'local>,
_this: JObject<'local>,
@@ -245,7 +245,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeDiscoveryPo
///
/// [`nativeDiscoveryStart`]: Java_io_unom_punktfunk_kit_NativeBridge_nativeDiscoveryStart
/// [`nativeDiscoveryPoll`]: Java_io_unom_punktfunk_kit_NativeBridge_nativeDiscoveryPoll
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeDiscoveryStop(
_env: JNIEnv,
_this: JObject,
+2 -2
View File
@@ -60,7 +60,7 @@ const TAG_HID_RAW: u8 = 0x05;
/// closed (all packed values are positive, so `-1` stays unambiguous). Kotlin routes the command
/// back to the controller holding that wire `pad` index (multi-pad rumble). Run from a Kotlin
/// poll thread.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeNextRumble(
_env: JNIEnv,
_this: JObject,
@@ -99,7 +99,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeNextRumble(
/// PlayerLeds → `[pad][0x02][bits]` (len 3)
/// Trigger → `[pad][0x03][which][effect…]` (len 3 + effect.len())
/// Returns the byte count written, or `-1` on timeout / session closed / buffer too small.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeNextHidout(
env: JNIEnv,
_this: JObject,
+3 -3
View File
@@ -54,7 +54,7 @@ mod probe;
/// on via quinn's defaults — forwards them as `log` records since no tracing subscriber is ever
/// installed. Android-only — there is no JVM (and no logcat) on the host build.
#[cfg(target_os = "android")]
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn JNI_OnLoad(
_vm: *mut jni::sys::JavaVM,
_reserved: *mut std::ffi::c_void,
@@ -74,7 +74,7 @@ pub extern "system" fn JNI_OnLoad(
/// `NativeBridge.abiVersion(): Int` — the core's C-ABI version. A non-error return is the
/// scaffold's proof that `System.loadLibrary` found the `.so`, the JNI symbol resolved, and the
/// linked `punktfunk-core` is the one we expect.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_abiVersion(
_env: JNIEnv,
_this: JObject,
@@ -83,7 +83,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_abiVersion(
}
/// `NativeBridge.coreVersion(): String` — the crate version, proving JNI string marshaling works.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_coreVersion<'local>(
env: JNIEnv<'local>,
_this: JObject<'local>,
+1 -1
View File
@@ -14,7 +14,7 @@ use std::time::Duration;
/// `NativeBridge.nativeProbe(host, port, timeoutMs): Boolean` — true if `host:port` completed a
/// QUIC handshake within `timeoutMs`. No pin/identity presented (trust-agnostic), mDNS-independent.
/// Blocking (builds its own runtime) — Kotlin runs it on `Dispatchers.IO`, never the main thread.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeProbe<'local>(
mut env: JNIEnv<'local>,
_this: JObject<'local>,
@@ -40,7 +40,7 @@ fn client(handle: jlong) -> Option<&'static SessionHandle> {
}
/// `NativeBridge.nativeClipSupported(handle)` — the host advertised `HOST_CAP_CLIPBOARD`.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeClipSupported(
_env: JNIEnv,
_this: JObject,
@@ -53,7 +53,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeClipSupport
/// `NativeBridge.nativeClipControl(handle, enabled)` — session-level opt-in/out. Nothing
/// clipboard-related happens on either side until an `enabled: true` crosses.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeClipControl(
_env: JNIEnv,
_this: JObject,
@@ -68,7 +68,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeClipControl
/// `NativeBridge.nativeClipOfferText(handle, seq)` — announce "the Android clipboard now holds
/// text" (format list only; bytes cross when the host fetches). `seq` is Kotlin's monotonic
/// counter, newest wins.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeClipOfferText(
_env: JNIEnv,
_this: JObject,
@@ -88,7 +88,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeClipOfferTe
/// `NativeBridge.nativeClipFetchText(handle, seq)` — pull the text of the host's offer `seq`.
/// Returns the transfer id echoed on the matching `data:`/`error:` event, or 1.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeClipFetchText(
_env: JNIEnv,
_this: JObject,
@@ -106,7 +106,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeClipFetchTe
/// `NativeBridge.nativeClipServeText(handle, reqId, text)` — answer a `fetch:` event with the
/// clipboard's current text (the host is pasting our offer).
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeClipServeText(
mut env: JNIEnv,
_this: JObject,
@@ -125,7 +125,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeClipServeTe
}
/// `NativeBridge.nativeClipCancel(handle, id)` — abort a transfer (either direction).
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeClipCancel(
_env: JNIEnv,
_this: JObject,
@@ -144,7 +144,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeClipCancel(
/// Text payloads ride `data:<xfer_id>:<text>` decoded lossily — safe because the phase-0
/// clipboard task delivers a whole payload in ONE event (`last = true`), so a chunk boundary
/// can never split a UTF-8 sequence.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeNextClip(
env: JNIEnv,
_this: JObject,
+10 -10
View File
@@ -36,7 +36,7 @@ fn note_error(e: &punktfunk_core::error::PunktfunkError) {
/// `NativeBridge.nativeTakeLastError(): String` — the machine token of the most recent failed
/// `nativeConnect`/`nativePair`, cleared on read (`""` when none). Call right after a `0`
/// handle / `""` fingerprint.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeTakeLastError<'local>(
env: JNIEnv<'local>,
_this: JObject<'local>,
@@ -51,7 +51,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeTakeLastErr
/// `NativeBridge.nativeGenerateIdentity(): String` — mint a fresh persistent self-signed identity.
/// Returns `"<certPem>\n-----PUNKTFUNK-KEY-----\n<keyPem>"`, or `""` on failure (logged). Kotlin
/// persists it (Keystore-wrapped) and only calls this again when the store is genuinely empty.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeGenerateIdentity<'local>(
env: JNIEnv<'local>,
_this: JObject<'local>,
@@ -74,7 +74,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeGenerateIde
/// the media sockets. Must be called BEFORE `nativeConnect` (the tag is applied at socket
/// creation); Kotlin's one connect choke point (`HostConnect.connectToHost`) does. The rest of the
/// toggle rides explicit per-session parameters (`nativeStartVideo` / `nativeStartAudio`).
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSetLowLatencyMode(
_env: JNIEnv,
_this: JObject,
@@ -120,7 +120,7 @@ fn force_parts_sysprop() -> bool {
/// budget: the normal path passes a short value, the no-PIN "request access" path a long one (≥ the
/// host's approval-park window) so a slow operator approval lands on this same parked connection
/// rather than timing the client out first. Returns an opaque handle, or 0 on failure.
#[no_mangle]
#[unsafe(no_mangle)]
#[allow(clippy::too_many_arguments)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'local>(
mut env: JNIEnv<'local>,
@@ -322,7 +322,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'lo
/// # Safety contract
/// `handle` must be `0` or a live handle from [`Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect`],
/// closed exactly once and not concurrently with other calls on the same handle (Kotlin owns this).
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeClose(
_env: JNIEnv,
_this: JObject,
@@ -344,7 +344,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeClose(
/// # Safety contract
/// `handle` must be `0` or a live handle from [`Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect`],
/// not freed / closed concurrently with this call (Kotlin still owns it and closes it via `nativeClose`).
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeDisconnectQuit(
_env: JNIEnv,
_this: JObject,
@@ -363,7 +363,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeDisconnectQ
/// `NativeBridge.nativeHostFingerprint(handle): String` — the SHA-256 (64-hex) of the cert the host
/// presented on this connection. Valid after a successful `nativeConnect`; Kotlin pins it on a TOFU
/// connect. `""` on a `0` handle.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeHostFingerprint<'local>(
env: JNIEnv<'local>,
_this: JObject<'local>,
@@ -388,7 +388,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeHostFingerp
/// Kotlin's stream watchdog polls this (~1 Hz) to leave a dead stream and return to the menu (where
/// the user can Wake-on-LAN the host) instead of stranding them on a frozen frame. `false` on a `0`
/// handle. Cheap (one atomic load); safe on the UI thread.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSessionEnded(
_env: JNIEnv,
_this: JObject,
@@ -413,7 +413,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSessionEnde
/// this existed the watchdog worded them identically ("the host may be asleep"), which is wrong for
/// every deliberate ending. `0` (NONE) on a `0` handle or before the session ends. Cheap (one
/// atomic load); safe on the UI thread.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeEndReason(
_env: JNIEnv,
_this: JObject,
@@ -433,7 +433,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeEndReason(
/// ceremony, presenting our persistent identity. On success returns the host's verified fingerprint
/// (64-hex) to persist + pin; on any failure (wrong PIN / MITM / host reject / unreachable) returns
/// `""` (logged). Blocking — Kotlin calls it off the UI thread.
#[no_mangle]
#[unsafe(no_mangle)]
#[allow(clippy::too_many_arguments)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativePair<'local>(
mut env: JNIEnv<'local>,
+18 -18
View File
@@ -35,7 +35,7 @@ fn send_event(handle: jlong, kind: InputKind, code: u32, x: i32, y: i32, flags:
}
/// `NativeBridge.nativeSendPointerMove(handle, dx, dy)` — relative mouse motion (screen +y down).
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendPointerMove(
_env: JNIEnv,
_this: JObject,
@@ -51,7 +51,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendPointer
/// normalizing against the size packed into `flags` as `(w << 16) | h` and mapping into the output
/// region (it drops the event if that size is zero). This is the touch "direct pointing" path — the
/// cursor jumps to the finger — and matches the Apple client's absolute touch forwarding.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendPointerAbs(
_env: JNIEnv,
_this: JObject,
@@ -68,7 +68,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendPointer
/// `NativeBridge.nativeSendPointerButton(handle, button, down)` — one button transition.
/// `button`: GameStream id (1=left, 2=middle, 3=right, 4=X1, 5=X2). `down`: 1=press, 0=release.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendPointerButton(
_env: JNIEnv,
_this: JObject,
@@ -86,7 +86,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendPointer
/// `NativeBridge.nativeSendScroll(handle, axis, delta)` — one scroll step. `axis`: 0=vertical,
/// 1=horizontal. `delta`: signed, WHEEL_DELTA(120)-scaled, +=up/right.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendScroll(
_env: JNIEnv,
_this: JObject,
@@ -103,7 +103,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendScroll(
/// surface, whose size rides in `flags` so the host can rescale into the output (identical
/// packing to MouseMoveAbs). On up only the id matters. The host injects a real touch contact
/// (libei touchscreen / wlroots / SendInput).
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendTouch(
_env: JNIEnv,
_this: JObject,
@@ -128,7 +128,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendTouch(
/// `NativeBridge.nativeSendKey(handle, vk, down, mods)` — one key transition. `vk`: Windows
/// Virtual-Key code (0 = unmapped → dropped). `down`: 1=press, 0=release. `mods`: VK modifier
/// bitmask (0 for now — the host folds modifiers from the L/R modifier key events themselves).
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendKey(
_env: JNIEnv,
_this: JObject,
@@ -151,7 +151,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendKey(
/// `NativeBridge.nativeTextInputSupported(handle)` — whether the host advertised
/// `HOST_CAP_TEXT_INPUT` (its inject backend types committed text), so the Kotlin side can pick
/// the real IME `InputConnection` over the TYPE_NULL raw-key fallback. `0` handle → false.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeTextInputSupported(
_env: JNIEnv,
_this: JObject,
@@ -168,7 +168,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeTextInputSu
/// `NativeBridge.nativeHostSupportsPen(handle)` — the host advertised `HOST_CAP_PEN`, so the
/// Kotlin side splits stylus pointers out of the touch path onto the pen plane
/// (design/pen-tablet-input.md §7). `0` handle → false.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeHostSupportsPen(
_env: JNIEnv,
_this: JObject,
@@ -197,7 +197,7 @@ const PEN_JNI_MAX_SAMPLES: usize = PEN_BATCH_MAX * 8;
/// normalized 0..1; `distance`/`tilt_deg`/`azimuth_deg`/`roll_deg` < 0 = unknown. Call only
/// against a [`nativeHostSupportsPen`] host; the client heartbeats the last sample ≤100 ms
/// while in range (Kotlin side — see `StylusStream`).
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendPen(
env: JNIEnv,
_this: JObject,
@@ -264,7 +264,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendPen(
/// Unicode scalar (`code` = the scalar; multi-char commits are consecutive events in order).
/// Control characters are skipped — Enter/Backspace/Tab ride the VK key path. Call only when
/// [`Java_io_unom_punktfunk_kit_NativeBridge_nativeTextInputSupported`] returned true.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendText(
mut env: JNIEnv,
_this: JObject,
@@ -296,7 +296,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendText(
/// `NativeBridge.nativeSendGamepadButton(handle, bit, down, pad)` — one gamepad button transition on
/// wire pad index `pad`. `bit`: a `gamepad::BTN_*` bit (e.g. BTN_A = 0x1000). `down`: 1=press,
/// 0=release. `pad`: wire pad index 0..15 (rides `flags`).
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendGamepadButton(
_env: JNIEnv,
_this: JObject,
@@ -318,7 +318,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendGamepad
/// `NativeBridge.nativeSendGamepadAxis(handle, axisId, value, pad)` — one gamepad axis update on wire
/// pad index `pad`. `axisId`: a `gamepad::AXIS_*` id (LS_X=0..RT=5). `value`: stick i16
/// (32768..32767, +y=up) or trigger 0..255. `pad`: wire pad index 0..15 (rides `flags`).
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendGamepadAxis(
_env: JNIEnv,
_this: JObject,
@@ -343,7 +343,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendGamepad
/// index 0..15 (rides `flags`). Sent ONCE when a pad opens, BEFORE any of its input; the core re-sends
/// it a few times against datagram loss, and an older host ignores the unknown tag (that pad then uses
/// the session-default kind from the handshake — the pre-existing single-pad behaviour on pad 0).
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendGamepadArrival(
_env: JNIEnv,
_this: JObject,
@@ -373,7 +373,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendGamepad
///
/// A `0` handle answers `true` — "don't suppress" is the safe answer when we cannot tell, matching
/// the `Auto` rule inside the predicate itself.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativePadMotionReaches(
_env: JNIEnv,
_this: JObject,
@@ -399,7 +399,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativePadMotionRe
/// unplugged so the host tears its virtual device down. `pad` (rides `flags`) is the only field; the
/// core stamps the per-pad seq (in the snapshot seq space, so a reordered snapshot can't resurrect the
/// pad) and arms a re-send burst against datagram loss. An older host ignores the unknown tag.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendGamepadRemove(
_env: JNIEnv,
_this: JObject,
@@ -415,7 +415,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendGamepad
/// `len` bytes are the report, id byte first (`0x42`/`0x45`/`0x47` state, `0x43` battery, …);
/// `len` is clamped to the 64-byte wire body. Called from the capture thread at the controller's
/// own report rate (~250500 Hz) — the direct-buffer read avoids a JNI array copy per report.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendPadHidReport(
env: JNIEnv,
_this: JObject,
@@ -455,7 +455,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendPadHidR
/// SCREEN convention (+y down — the wire's fixed meaning); `active` 0 lifts the finger. The
/// host's DualSense-family backends scale onto the virtual pad's touch surface. On-change only —
/// the capture diffs, the host holds per-slot state.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendPadTouch(
_env: JNIEnv,
_this: JObject,
@@ -485,7 +485,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendPadTouc
/// raw signed-16 values in the pad's own units, passed straight into the host's virtual
/// DualSense report (the wire is a unit passthrough). Called from the capture thread at the
/// controller's report rate.
#[no_mangle]
#[unsafe(no_mangle)]
#[allow(clippy::too_many_arguments)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSendPadMotion(
_env: JNIEnv,
+17 -17
View File
@@ -19,7 +19,7 @@ use super::{jni_guard, lock_recover, SessionHandle};
/// presenter's intent (0 = lowest latency / 1 = smoothness; buffer 0 = auto, 1..=3 frames).
/// No-op if already started.
#[cfg(target_os = "android")]
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStartVideo(
mut env: JNIEnv,
_this: JObject,
@@ -91,7 +91,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStartVideo(
/// decoders for it before calling [`Java_io_unom_punktfunk_kit_NativeBridge_nativeStartVideo`].
/// Empty string on a `0` handle. Cheap; safe on the UI thread.
#[cfg(target_os = "android")]
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoMime<'local>(
env: JNIEnv<'local>,
_this: JObject<'local>,
@@ -116,7 +116,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoMime<'
/// collapses PyroWave onto `video/hevc` and can't name it. Empty string on a `0` handle. Cheap;
/// safe on the UI thread. Android-gated (reads `crate::decode`), matching `nativeVideoMime`.
#[cfg(target_os = "android")]
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoCodecLabel<'local>(
env: JNIEnv<'local>,
_this: JObject<'local>,
@@ -140,7 +140,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoCodecL
/// One-shot (the decoder is fixed for the session); poll once after the HUD appears. Not
/// android-gated — pure `jni` + a lock, so it links on the host build too (Kotlin only calls it on
/// device).
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoDecoderLabel<'local>(
env: JNIEnv<'local>,
_this: JObject<'local>,
@@ -161,7 +161,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoDecode
/// `NativeBridge.nativeStopVideo(handle)` — stop + join the decode thread (without closing the
/// session). No-op on `0`.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStopVideo(
_env: JNIEnv,
_this: JObject,
@@ -210,7 +210,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStopVideo(
/// Poll ~1 Hz from the UI; each call
/// resets the measurement window. Not android-gated — pure `jni` + connector reads, so it links on
/// the host build too (Kotlin only ever calls it on device).
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoStats(
env: JNIEnv,
_this: JObject,
@@ -312,7 +312,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoStats(
/// trailing `refreshHz` was appended later — old readers index only 0/1 and never see it. `null`
/// on a `0` handle. Not android-gated — pure `jni` + a connector read, so it links on the host
/// build too. Cheap; safe on the UI thread.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoSize(
env: JNIEnv,
_this: JObject,
@@ -346,7 +346,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeVideoSize(
/// Enabling resets the measurement window so a later show never reports stale data. Sticky for the
/// session (survives video stop/start across surface recreation). No-op on `0`. Not android-gated —
/// pure `jni` + an atomic store, so it links on the host build too.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSetVideoStatsEnabled(
_env: JNIEnv,
_this: JObject,
@@ -373,7 +373,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSetVideoSta
/// routing. No-op if already started or on a `0` handle. Best-effort: a failure leaves video
/// streaming.
#[cfg(target_os = "android")]
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStartAudio(
_env: JNIEnv,
_this: JObject,
@@ -398,7 +398,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStartAudio(
/// `NativeBridge.nativeStopAudio(handle)` — stop + join the audio thread and close AAudio (without
/// closing the session). No-op on `0`.
#[cfg(target_os = "android")]
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStopAudio(
_env: JNIEnv,
_this: JObject,
@@ -422,7 +422,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStopAudio(
/// the running capture's id. Caller MUST hold RECORD_AUDIO; a failure (e.g. no permission) leaves
/// the rest of the session streaming.
#[cfg(target_os = "android")]
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStartMic(
_env: JNIEnv,
_this: JObject,
@@ -457,7 +457,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStartMic(
/// stream (without closing the session). No-op on `0`. Leaves the session's mute state alone: a
/// surface recreate stops and restarts the mic, and a user who muted must stay muted through it.
#[cfg(target_os = "android")]
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStopMic(
_env: JNIEnv,
_this: JObject,
@@ -484,7 +484,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStopMic(
/// start. A kernel that refuses the interface claim is NOT reported here — the renderer discovers
/// that on its own thread and degrades to tier C, because some OEM kernels refuse and there is no
/// app-side fix worth blocking a session on.
#[no_mangle]
#[unsafe(no_mangle)]
#[cfg(target_os = "android")]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStartPadAudio(
_env: JNIEnv,
@@ -530,7 +530,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStartPadAud
/// The check a standalone harness cannot make: it owns its descriptor by construction, so it can
/// never reveal that the client handed the renderer a descriptor something else was already
/// driving. Returns sample frames written, or negative on failure (see `pad_audio::SelfTest`).
#[no_mangle]
#[unsafe(no_mangle)]
#[cfg(target_os = "android")]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativePadAudioSelfTest(
_env: JNIEnv,
@@ -553,7 +553,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativePadAudioSel
///
/// Returns only once the render thread is joined, which is the point: Kotlin may close the
/// `UsbDeviceConnection` as soon as this returns and not before.
#[no_mangle]
#[unsafe(no_mangle)]
#[cfg(target_os = "android")]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStopPadAudio(
_env: JNIEnv,
@@ -594,7 +594,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeStopPadAudi
/// One honest consequence of keeping the stream open: the platform's own recording indicator stays
/// lit while muted, because the mic really is still open. What stops is the encode and the send —
/// no captured audio leaves the process.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSetMicMuted(
_env: JNIEnv,
_this: JObject,
@@ -617,7 +617,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSetMicMuted
/// refused every AAudio input rung (or a missing RECORD_AUDIO grant) shows no control instead of a
/// lie about a mic that is being heard. `false` on a `0` handle. Cheap (one uncontended lock).
#[cfg(target_os = "android")]
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeMicActive(
_env: JNIEnv,
_this: JObject,
+2 -2
View File
@@ -23,7 +23,7 @@ const PROBE_RESULT_LEN: usize = 6;
/// **briefly pausing video**. Non-blocking: poll
/// [`Java_io_unom_punktfunk_kit_NativeBridge_nativeProbeResult`] until its `done` element is 1.
/// Starting a probe resets any prior measurement. `false` on a `0` handle or a closed session.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSpeedTest(
_env: JNIEnv,
_this: JObject,
@@ -54,7 +54,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSpeedTest(
///
/// Layout (doubles so one array carries both the counts and the percentages):
/// `[done, throughputKbps, lossPct, hostDropPct, elapsedMs, recvBytes]`.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeProbeResult<'local>(
env: JNIEnv<'local>,
_this: JObject<'local>,
+1 -1
View File
@@ -10,7 +10,7 @@ use jni::JNIEnv;
/// magic packet. `macsCsv` is comma-separated MACs (`aa:bb:..,cc:dd:..`, learned from the host's
/// mDNS `mac` TXT while it was online); `lastIp` is the host's last-known IPv4 (or empty).
/// Returns true if at least one datagram went out.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeWakeOnLan<'local>(
mut env: JNIEnv<'local>,
_this: JObject<'local>,
+3 -1
View File
@@ -885,7 +885,9 @@ pub fn run() -> glib::ExitCode {
] {
if let Ok(v) = std::env::var(var) {
tracing::info!(var, value = %v, "clearing Steam's SDL device filter");
std::env::remove_var(var);
// SAFETY: top of `run()`, before GTK init or any other thread exists in this
// process — nothing reads the environment concurrently.
unsafe { std::env::remove_var(var) };
}
}
// Headless paths (no GTK window).
+4 -1
View File
@@ -135,7 +135,10 @@ mod tests {
let home = std::env::temp_dir().join(format!("pf-spawn-test-{}", std::process::id()));
let cfg = home.join(".config/punktfunk");
std::fs::create_dir_all(&cfg).unwrap();
std::env::set_var("HOME", &home);
// SAFETY: the only env-mutating test in this binary (see the doc above — one test, one
// `HOME`, deliberately not split). Parallel tests may `getenv` concurrently; glibc keeps
// replaced environ storage alive, and every reader tolerates either value.
unsafe { std::env::set_var("HOME", &home) };
// A device whose owner has set a bitrate, and a host bound to a profile that raises it
// further — the two layers the spec has to carry.
+14 -4
View File
@@ -509,8 +509,13 @@ mod session_main {
const TOKEN: &str = "video_decode";
match std::env::var("RADV_PERFTEST") {
Ok(v) if v.split(',').any(|t| t == TOKEN) => return,
Ok(v) if !v.is_empty() => std::env::set_var("RADV_PERFTEST", format!("{v},{TOKEN}")),
_ => std::env::set_var("RADV_PERFTEST", TOKEN),
// SAFETY: called at the very top of `run()`, before this process creates any
// thread — the Vulkan loader, SDL, and the session runtime all start later.
Ok(v) if !v.is_empty() => unsafe {
std::env::set_var("RADV_PERFTEST", format!("{v},{TOKEN}"))
},
// SAFETY: as above — single-threaded startup.
_ => unsafe { std::env::set_var("RADV_PERFTEST", TOKEN) },
}
tracing::info!(
radv_perftest = %std::env::var("RADV_PERFTEST").unwrap_or_default(),
@@ -804,7 +809,10 @@ mod session_main {
("PUNKTFUNK_AUDIO_SOURCE", &s.mic_device),
] {
if std::env::var_os(var).is_none() && !value.is_empty() {
std::env::set_var(var, value);
// SAFETY: still the single-threaded startup stretch of `run()` — the
// early-exit probes above return out of the process, and everything that
// spawns threads (the session, the console, SDL) only starts below.
unsafe { std::env::set_var(var, value) };
}
}
}
@@ -818,7 +826,9 @@ mod session_main {
] {
if let Ok(v) = std::env::var(var) {
tracing::info!(var, value = %v, "clearing Steam's SDL device filter");
std::env::remove_var(var);
// SAFETY: as the settings block above — single-threaded startup, before SDL
// (the reader of these variables) or any other thread exists.
unsafe { std::env::remove_var(var) };
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ name = "punktfunk-client-windows"
description = "Native Windows punktfunk/1 client — WinUI 3 (windows-reactor) shell, SDL3 gamepads; streaming runs in the spawned punktfunk-session binary"
version.workspace = true
edition.workspace = true
# Not workspace-inherited (1.82): windows-reactor at the pinned rev declares rust-version 1.95+
# Not workspace-inherited (1.85): windows-reactor at the pinned rev declares rust-version 1.95+
# and edition 2024. rust-toolchain.toml pins 1.96, so this records reality rather than raising it.
rust-version = "1.96"
license.workspace = true
+10 -10
View File
@@ -318,10 +318,10 @@ fn edit_editor(
if !addr.is_empty() {
h.addr = addr;
}
if let Ok(p) = port_draft.borrow().trim().parse::<u16>() {
if p != 0 {
h.port = p;
}
if let Ok(p) = port_draft.borrow().trim().parse::<u16>()
&& p != 0
{
h.port = p;
}
let mac = mac_draft.borrow().trim().to_string();
h.mac = if mac.is_empty() {
@@ -1094,12 +1094,12 @@ pub(crate) fn hosts_page(props: &HostsProps, cx: &mut RenderCx) -> Element {
.close_button_text("Cancel")
.is_open(pending.is_some())
.on_closed(move |r: ContentDialogResult| {
if r == ContentDialogResult::Primary {
if let Some((fp, _)) = &pending {
let mut known = KnownHosts::load();
known.remove_by_fp(fp);
let _ = known.save();
}
if r == ContentDialogResult::Primary
&& let Some((fp, _)) = &pending
{
let mut known = KnownHosts::load();
known.remove_by_fp(fp);
let _ = known.save();
}
sf.call(None); // re-renders the page; the row is gone on the next load
})
+40 -37
View File
@@ -515,35 +515,37 @@ fn root(cx: &mut RenderCx, ctx: &Arc<AppCtx>) -> Element {
move || {
std::thread::Builder::new()
.name("pf-probe".into())
.spawn(move || loop {
// A spawned session/browse child is running: the shell is hidden
// (nobody sees the pips) and one of these hosts is mid-stream —
// probing it is pure noise. Sleep through and sweep after it ends.
if shared.session.lock().unwrap().is_running() {
std::thread::sleep(Duration::from_secs(12));
continue;
}
let handles: Vec<_> = KnownHosts::load()
.hosts
.into_iter()
.filter(|h| !h.addr.is_empty())
.map(|h| {
std::thread::spawn(move || {
(
h.fp_hex,
NativeClient::probe(
&h.addr,
h.port,
Duration::from_millis(2500),
),
)
.spawn(move || {
loop {
// A spawned session/browse child is running: the shell is hidden
// (nobody sees the pips) and one of these hosts is mid-stream —
// probing it is pure noise. Sleep through and sweep after it ends.
if shared.session.lock().unwrap().is_running() {
std::thread::sleep(Duration::from_secs(12));
continue;
}
let handles: Vec<_> = KnownHosts::load()
.hosts
.into_iter()
.filter(|h| !h.addr.is_empty())
.map(|h| {
std::thread::spawn(move || {
(
h.fp_hex,
NativeClient::probe(
&h.addr,
h.port,
Duration::from_millis(2500),
),
)
})
})
})
.collect();
let map: HashMap<String, bool> =
handles.into_iter().filter_map(|h| h.join().ok()).collect();
set_probed.call(map);
std::thread::sleep(Duration::from_secs(12));
.collect();
let map: HashMap<String, bool> =
handles.into_iter().filter_map(|h| h.join().ok()).collect();
set_probed.call(map);
std::thread::sleep(Duration::from_secs(12));
}
})
.ok();
}
@@ -560,14 +562,15 @@ fn root(cx: &mut RenderCx, ctx: &Arc<AppCtx>) -> Element {
let anim_gen = cx.use_ref(std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)));
let (anim, set_anim) = cx.use_async_state((Option::<Screen>::None, 1.0f64));
cx.use_effect(screen.clone(), {
let (s, set_anim, gen) = (screen.clone(), set_anim.clone(), anim_gen.borrow().clone());
let (s, set_anim, generation) =
(screen.clone(), set_anim.clone(), anim_gen.borrow().clone());
move || {
use std::sync::atomic::Ordering::SeqCst;
let mine = gen.fetch_add(1, SeqCst) + 1;
let mine = generation.fetch_add(1, SeqCst) + 1;
std::thread::spawn(move || {
const STEPS: u32 = 14;
for i in 0..=STEPS {
if gen.load(SeqCst) != mine {
if generation.load(SeqCst) != mine {
return; // a newer navigation superseded this tween
}
let p = f64::from(i) / f64::from(STEPS);
@@ -593,18 +596,18 @@ fn root(cx: &mut RenderCx, ctx: &Arc<AppCtx>) -> Element {
let nav_gen = cx.use_ref(std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)));
let (nav_anim, set_nav_anim) = cx.use_async_state((String::new(), 1.0f64));
cx.use_effect(settings_nav.clone(), {
let (s, set_nav_anim, gen) = (
let (s, set_nav_anim, generation) = (
settings_nav.clone(),
set_nav_anim.clone(),
nav_gen.borrow().clone(),
);
move || {
use std::sync::atomic::Ordering::SeqCst;
let mine = gen.fetch_add(1, SeqCst) + 1;
let mine = generation.fetch_add(1, SeqCst) + 1;
std::thread::spawn(move || {
const STEPS: u32 = 14;
for i in 0..=STEPS {
if gen.load(SeqCst) != mine {
if generation.load(SeqCst) != mine {
return; // a newer section switch superseded this tween
}
let p = f64::from(i) / f64::from(STEPS);
@@ -628,10 +631,10 @@ fn root(cx: &mut RenderCx, ctx: &Arc<AppCtx>) -> Element {
let add_gen = cx.use_ref(std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)));
let (add_anim, set_add_anim) = cx.use_async_state(0.0f64);
cx.use_effect(show_add, {
let (set_add_anim, gen) = (set_add_anim.clone(), add_gen.borrow().clone());
let (set_add_anim, generation) = (set_add_anim.clone(), add_gen.borrow().clone());
move || {
use std::sync::atomic::Ordering::SeqCst;
let mine = gen.fetch_add(1, SeqCst) + 1;
let mine = generation.fetch_add(1, SeqCst) + 1;
if !show_add {
set_add_anim.call(0.0);
return;
@@ -639,7 +642,7 @@ fn root(cx: &mut RenderCx, ctx: &Arc<AppCtx>) -> Element {
std::thread::spawn(move || {
const STEPS: u32 = 12;
for i in 0..=STEPS {
if gen.load(SeqCst) != mine {
if generation.load(SeqCst) != mine {
return; // reopened/closed mid-tween — a newer run owns the value
}
let p = f64::from(i) / f64::from(STEPS);
+5 -4
View File
@@ -82,10 +82,11 @@ fn main() {
// where the user's hosts already are. A hand-off that finds nobody falls through and this
// process becomes the shell that opens it, so the link is never simply lost.
let link = deeplink::positional_url(&args);
if let Some(url) = &link {
if !deeplink::claim_primary() && deeplink::forward_to_primary(url) {
return;
}
if let Some(url) = &link
&& !deeplink::claim_primary()
&& deeplink::forward_to_primary(url)
{
return;
}
if flag("--discover") {
+1 -1
View File
@@ -7,7 +7,7 @@
[package]
name = "pf-capture"
version.workspace = true
edition = "2021"
edition.workspace = true
rust-version.workspace = true
license = "MIT OR Apache-2.0"
description = "punktfunk host frame capture: Linux PipeWire portal + Windows IDD direct-push capturers behind one Capturer trait."
+1 -1
View File
@@ -64,7 +64,7 @@ pub(crate) fn hybrid_hook_hits() -> u64 {
// on the main thread but DXGI runs the hooked export from the encode/worker thread (possibly a
// different core), so the "same-thread, no flush needed" assumption was wrong.
#[link(name = "kernel32")]
extern "system" {
unsafe extern "system" {
fn FlushInstructionCache(h: *mut c_void, base: *const c_void, size: usize) -> i32;
fn GetCurrentProcess() -> *mut c_void;
}
+4 -4
View File
@@ -164,10 +164,10 @@ fn read_appid(conn: &RustConnection, root: Window, atom: Atom) -> Option<u32> {
.ok()?
.reply()
.ok()?;
// Bound rather than returned inline: the iterator borrows `reply`, and as a tail
// expression its temporary would outlive it.
let id = reply.value32()?.next();
id
// Inline is sound since edition 2024: tail-expression temporaries now drop BEFORE the
// block's locals, so the iterator borrowing `reply` no longer outlives it (the 2021 rule
// forced a `let` binding here).
reply.value32()?.next()
}
/// The whole decision, separated from X so it can be tested: an overlay is up exactly when
@@ -2241,7 +2241,7 @@ mod parity {
// `desc.Height` rows at `RowPitch` and the chroma plane follows at byte
// offset `RowPitch * desc.Height`, so `total` below is exactly the mapped
// extent and every sub-slice read is inside it. `Unmap` pairs the `Map`.
let out = unsafe {
unsafe {
let src: ID3D11Resource = pool.cast().expect("pool -> resource");
let dst: ID3D11Resource = staging.cast().expect("staging -> resource");
self.ctx
@@ -2268,8 +2268,7 @@ mod parity {
}
self.ctx.Unmap(&staging, 0);
out
};
out
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
[package]
name = "pf-clipboard"
version.workspace = true
edition = "2021"
edition.workspace = true
rust-version.workspace = true
license = "MIT OR Apache-2.0"
description = "punktfunk host shared clipboard: per-OS session-clipboard backends behind one HostClipboard + the QUIC clipboard-plane coordinator."
+8 -4
View File
@@ -382,13 +382,13 @@ impl SettingsScreen {
}
ListMsg::Adjust(_) => Some(MenuPulse::Boundary),
ListMsg::None => pulse,
}
};
}
RowId::NoProfiles => {
return match msg {
ListMsg::Adjust(_) | ListMsg::Activate => Some(MenuPulse::Boundary),
ListMsg::None => pulse,
}
};
}
_ => {}
}
@@ -1054,12 +1054,16 @@ mod tests {
fn fake_home() {
use std::sync::OnceLock;
static HOME: OnceLock<std::path::PathBuf> = OnceLock::new();
let dir = HOME.get_or_init(|| {
HOME.get_or_init(|| {
let dir = std::env::temp_dir().join(format!("pf-settings-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
// SAFETY: runs at most once, inside `get_or_init` — concurrent `fake_home` callers
// block until it returns, and nothing else in this binary mutates `HOME`. (The old
// set after the closure ran on EVERY call, so two parallel tests could race the
// write; setting once under the OnceLock is what makes this sound.)
unsafe { std::env::set_var("HOME", &dir) };
dir
});
std::env::set_var("HOME", dir);
}
/// Render the screen once so its strip and list carry real geometry, then hand back a
+7 -4
View File
@@ -49,13 +49,16 @@ fn motion_matches_the_shared_vectors() {
fn fake_home() {
use std::sync::OnceLock;
static HOME: OnceLock<std::path::PathBuf> = OnceLock::new();
let dir = HOME.get_or_init(|| {
HOME.get_or_init(|| {
let dir = std::env::temp_dir().join(format!("pf-console-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::env::set_var("HOME", &dir);
dir.clone()
// SAFETY: runs at most once, inside `get_or_init` — concurrent `fake_home` callers
// block until it returns, and nothing else in this binary mutates `HOME`. (The old
// re-set after the closure ran on EVERY call, so two parallel tests could race the
// write; setting once under the OnceLock is what makes this sound.)
unsafe { std::env::set_var("HOME", &dir) };
dir
});
std::env::set_var("HOME", dir);
}
fn hosts() -> Vec<HostRow> {
+2 -2
View File
@@ -9,8 +9,8 @@
[package]
name = "pf-driver-proto"
version = "0.0.1"
edition = "2021"
rust-version = "1.82"
edition.workspace = true
rust-version.workspace = true
license = "MIT OR Apache-2.0"
description = "Shared host<->driver binary contract for the punktfunk pf-vdisplay virtual display (control IOCTLs + IDD-push frame transport)."
publish = false
+1 -1
View File
@@ -6,7 +6,7 @@
[package]
name = "pf-encode"
version.workspace = true
edition = "2021"
edition.workspace = true
rust-version.workspace = true
license = "MIT OR Apache-2.0"
description = "punktfunk host video encode: NVENC/VAAPI/AMF/QSV/Vulkan-Video/PyroWave/openh264 backends behind one Encoder trait."
+74 -58
View File
@@ -1652,7 +1652,7 @@ impl NvencCudaEncoder {
return Err(nvenc_status::call_err(
"register_resource (CUDADEVICEPTR)",
e,
))
));
}
}
self.ring.push(RingSlot {
@@ -2779,6 +2779,20 @@ mod tests {
use pf_frame::{CapturedFrame, FramePayload, PixelFormat};
use pf_zerocopy::cuda::DeviceBuffer;
/// Env knob for the `#[ignore]`d hardware spikes, which every caller's doc says to run ALONE
/// with `--test-threads=1` (they mutate process env and own the GPU).
fn set_env(key: &str, val: impl AsRef<std::ffi::OsStr>) {
// SAFETY: only reached from the manually-run `--test-threads=1` hardware tests, so no
// other thread exists in this process to read or write the environment concurrently.
unsafe { std::env::set_var(key, val) };
}
/// [`set_env`]'s companion; the same single-threaded-run contract.
fn remove_env(key: &str) {
// SAFETY: as `set_env` — single-threaded manual test run, no concurrent env access.
unsafe { std::env::remove_var(key) };
}
/// The 10-bit input mapping is load-bearing in a way a smoke test can't reach: pick the wrong
/// NVENC format for a packed 2:10:10:10 capture and the encoder reads the words as 8-bit
/// `ARGB` — a picture that decodes, looks *almost* right, and is silently 8-bit with the
@@ -3315,8 +3329,8 @@ mod tests {
// Isolate the split variable: sub-frame off, and open explicitly split-DISABLED so the
// switch below is a real change rather than a no-op.
std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "0");
std::env::set_var("PUNKTFUNK_SPLIT_ENCODE", "0");
set_env("PUNKTFUNK_NVENC_SUBFRAME", "0");
set_env("PUNKTFUNK_SPLIT_ENCODE", "0");
pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
let mut enc = NvencCudaEncoder::open(
@@ -3428,8 +3442,8 @@ mod tests {
}
enc.flush().ok();
std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE");
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
remove_env("PUNKTFUNK_SPLIT_ENCODE");
remove_env("PUNKTFUNK_NVENC_SUBFRAME");
}
/// ON-HARDWARE — **spike S1b**, the other half of S1: an in-place `splitEncodeMode` change that
@@ -3468,7 +3482,7 @@ mod tests {
const SETTLE: u32 = 16;
let two = M::NV_ENC_SPLIT_TWO_FORCED_MODE as u32;
std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "0");
set_env("PUNKTFUNK_NVENC_SUBFRAME", "0");
// Separate buffers rotated per frame, so identical content can't let the encoder
// skip-code everything and erase the difference we are trying to measure.
@@ -3482,7 +3496,7 @@ mod tests {
// Returns (early-half p50 µs, late-half p50 µs, median bytes/AU).
let run_leg = |open_split: &str, switch_to: Option<u32>| -> (u128, u128, usize) {
std::env::set_var("PUNKTFUNK_SPLIT_ENCODE", open_split);
set_env("PUNKTFUNK_SPLIT_ENCODE", open_split);
let mut enc = NvencCudaEncoder::open(
Codec::H265,
PixelFormat::Nv12,
@@ -3554,9 +3568,15 @@ mod tests {
println!("S1b @ {W}x{H}@60 HEVC 8-bit, {} Mbps CBR:", BPS / 1_000_000);
println!(" (early = first half of the measured window, late = second half)");
println!(" A fresh DISABLE : early {a_early:>6} late {a_late:>6} us/frame, {a_bytes:>8} B/AU");
println!(" B fresh TWO_FORCED : early {b_early:>6} late {b_late:>6} us/frame, {b_bytes:>8} B/AU");
println!(" C DISABLE→TWO in situ: early {c_early:>6} late {c_late:>6} us/frame, {c_bytes:>8} B/AU");
println!(
" A fresh DISABLE : early {a_early:>6} late {a_late:>6} us/frame, {a_bytes:>8} B/AU"
);
println!(
" B fresh TWO_FORCED : early {b_early:>6} late {b_late:>6} us/frame, {b_bytes:>8} B/AU"
);
println!(
" C DISABLE→TWO in situ: early {c_early:>6} late {c_late:>6} us/frame, {c_bytes:>8} B/AU"
);
if c_early > c_late + c_late / 8 {
println!(
" ⇒ leg C SETTLES ({c_early} → {c_late} us): the in-place switch is not \
@@ -3583,8 +3603,8 @@ mod tests {
}
);
std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE");
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
remove_env("PUNKTFUNK_SPLIT_ENCODE");
remove_env("PUNKTFUNK_NVENC_SUBFRAME");
let _ = (a_bytes, b_bytes, c_bytes);
}
@@ -3618,8 +3638,8 @@ mod tests {
// Open split-DISABLED, and leave sub-frame at its Linux default (ON where the GPU
// advertises SUBFRAME_READBACK) — that is the fleet shape the arbitration starts from.
std::env::set_var("PUNKTFUNK_SPLIT_ENCODE", "0");
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
set_env("PUNKTFUNK_SPLIT_ENCODE", "0");
remove_env("PUNKTFUNK_NVENC_SUBFRAME");
pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
let mut enc = NvencCudaEncoder::open(
@@ -3664,7 +3684,7 @@ mod tests {
"S1c SKIPPED: sub-frame is off at open on this GPU/driver, so there is no pair to \
flip the arbitration reduces to S1a's plain split switch here."
);
std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE");
remove_env("PUNKTFUNK_SPLIT_ENCODE");
return;
}
@@ -3715,7 +3735,7 @@ mod tests {
}
enc.flush().ok();
std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE");
remove_env("PUNKTFUNK_SPLIT_ENCODE");
}
/// ON-HARDWARE — **the D5 confirm** (design §2 defect D5), the one claim in that list that was
@@ -3755,12 +3775,12 @@ mod tests {
// produced a spurious "D5 REFUTED" on the first run of this test.
let run = |split: Option<&str>, subframe: Option<&str>| -> (u128, bool) {
match split {
Some(v) => std::env::set_var("PUNKTFUNK_SPLIT_ENCODE", v),
None => std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE"),
Some(v) => set_env("PUNKTFUNK_SPLIT_ENCODE", v),
None => remove_env("PUNKTFUNK_SPLIT_ENCODE"),
}
match subframe {
Some(v) => std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", v),
None => std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME"),
Some(v) => set_env("PUNKTFUNK_NVENC_SUBFRAME", v),
None => remove_env("PUNKTFUNK_NVENC_SUBFRAME"),
}
let mut enc = NvencCudaEncoder::open(
Codec::H265,
@@ -3841,8 +3861,8 @@ mod tests {
}
);
std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE");
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
remove_env("PUNKTFUNK_SPLIT_ENCODE");
remove_env("PUNKTFUNK_NVENC_SUBFRAME");
}
/// ON-HARDWARE — **what is the real split ceiling on this GPU?** Feeds WP1.1: we want to use
@@ -3870,13 +3890,13 @@ mod tests {
const WARMUP: u32 = 8;
const MEASURED: u32 = 24;
std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "0");
set_env("PUNKTFUNK_NVENC_SUBFRAME", "0");
pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
let frames: Vec<CapturedFrame> = (0..4).map(|i| nv12_frame(W, H, i)).collect();
// → (requested mode, mode actually opened, p50 µs, engines the driver reports)
let run = |split: &str| -> (u32, u128, i32) {
std::env::set_var("PUNKTFUNK_SPLIT_ENCODE", split);
set_env("PUNKTFUNK_SPLIT_ENCODE", split);
let mut enc = NvencCudaEncoder::open(
Codec::H265,
PixelFormat::Nv12,
@@ -3940,11 +3960,7 @@ mod tests {
};
println!(
" req {label} → opened_mode={opened:<2} {} {us:>6} us/frame{vs} [engines={engines}]",
if honoured {
"HONOURED"
} else {
"FELL BACK"
}
if honoured { "HONOURED" } else { "FELL BACK" }
);
}
println!(
@@ -3952,8 +3968,8 @@ mod tests {
HONOURED but no faster than DISABLE was accepted and did nothing."
);
std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE");
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
remove_env("PUNKTFUNK_SPLIT_ENCODE");
remove_env("PUNKTFUNK_NVENC_SUBFRAME");
}
/// ON-HARDWARE — the live split arbitration end to end (WP3). Opens a 4K session that the
@@ -3975,9 +3991,9 @@ mod tests {
const H: u32 = 2160;
let disable = nv::NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_DISABLE_MODE as u32;
std::env::set_var("PUNKTFUNK_NVENC_SPLIT_ARBITRATE", "1");
std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "0");
std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE");
set_env("PUNKTFUNK_NVENC_SPLIT_ARBITRATE", "1");
set_env("PUNKTFUNK_NVENC_SUBFRAME", "0");
remove_env("PUNKTFUNK_SPLIT_ENCODE");
pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
let frames: Vec<CapturedFrame> = (0..4).map(|i| nv12_frame(W, H, i)).collect();
@@ -4045,8 +4061,8 @@ mod tests {
max_forced_split_mode(enc_engines)
);
std::env::remove_var("PUNKTFUNK_NVENC_SPLIT_ARBITRATE");
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
remove_env("PUNKTFUNK_NVENC_SPLIT_ARBITRATE");
remove_env("PUNKTFUNK_NVENC_SUBFRAME");
// The verdict cache is process-global: leaving this session's result in it would steer
// every later test that opens the same config with the split env unset (the D5 legs do
// exactly that).
@@ -4086,14 +4102,14 @@ mod tests {
})
.unwrap_or((3840, 2160, 60));
std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "0");
set_env("PUNKTFUNK_NVENC_SUBFRAME", "0");
pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
// 10-bit input: the packed 2:10:10:10 PQ path is how a Main10 session is actually fed here
// (`bit_depth`/`hdr` are DERIVED from the input format, never trusted from the args).
let frames: Vec<CapturedFrame> = (0..4).map(|i| rgb10_frame(w, h, i)).collect();
let run = |split: &str| -> (u128, u8, usize) {
std::env::set_var("PUNKTFUNK_SPLIT_ENCODE", split);
set_env("PUNKTFUNK_SPLIT_ENCODE", split);
let mut enc = NvencCudaEncoder::open(
Codec::H265,
PixelFormat::X2Rgb10,
@@ -4157,8 +4173,8 @@ mod tests {
}
);
std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE");
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
remove_env("PUNKTFUNK_SPLIT_ENCODE");
remove_env("PUNKTFUNK_NVENC_SUBFRAME");
}
/// ON-HARDWARE — **THE BITS/FRAME CURVE**, the measurement this whole programme has been blind
@@ -4190,7 +4206,7 @@ mod tests {
})
.unwrap_or((3840, 2160, 60));
std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "0");
set_env("PUNKTFUNK_NVENC_SUBFRAME", "0");
pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
// Sweep CONTENT DETAIL, not nominal bitrate. Pure noise is incompressible, so a low
// bitrate target simply overshoots (measured: 719 KB/AU against a 104 KB quota) and every
@@ -4207,7 +4223,7 @@ mod tests {
let frames: Vec<CapturedFrame> =
(0..4).map(|i| noise_nv12_frame(w, h, i, block)).collect();
let run = |split: &str| -> (u128, usize) {
std::env::set_var("PUNKTFUNK_SPLIT_ENCODE", split);
set_env("PUNKTFUNK_SPLIT_ENCODE", split);
let mut enc = NvencCudaEncoder::open(
Codec::H265,
PixelFormat::Nv12,
@@ -4250,8 +4266,8 @@ mod tests {
);
}
std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE");
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
remove_env("PUNKTFUNK_SPLIT_ENCODE");
remove_env("PUNKTFUNK_NVENC_SUBFRAME");
}
/// A pre-session RFI request and nonsense ranges all correctly decline (→ caller forces IDR).
@@ -4658,12 +4674,12 @@ mod tests {
struct EnvGuard;
impl Drop for EnvGuard {
fn drop(&mut self) {
std::env::remove_var("PUNKTFUNK_NVENC_SLICES");
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
remove_env("PUNKTFUNK_NVENC_SLICES");
remove_env("PUNKTFUNK_NVENC_SUBFRAME");
}
}
std::env::set_var("PUNKTFUNK_NVENC_SLICES", "4");
std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "1");
set_env("PUNKTFUNK_NVENC_SLICES", "4");
set_env("PUNKTFUNK_NVENC_SUBFRAME", "1");
let _guard = EnvGuard;
pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
@@ -4770,8 +4786,8 @@ mod tests {
const W: u32 = 1920;
const H: u32 = 1080;
// Defaults under test — make sure another test's knobs aren't leaking in.
std::env::remove_var("PUNKTFUNK_NVENC_SLICES");
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
remove_env("PUNKTFUNK_NVENC_SLICES");
remove_env("PUNKTFUNK_NVENC_SUBFRAME");
pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
let mut enc = NvencCudaEncoder::open(
@@ -4866,8 +4882,8 @@ mod tests {
const W: u32 = 1920;
const H: u32 = 1080;
// The ceiling under test is the negotiated one, not the operator override.
std::env::remove_var("PUNKTFUNK_NVENC_SLICES");
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
remove_env("PUNKTFUNK_NVENC_SLICES");
remove_env("PUNKTFUNK_NVENC_SUBFRAME");
pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
let mut enc = NvencCudaEncoder::open(
Codec::H265,
@@ -4911,16 +4927,16 @@ mod tests {
struct EnvGuard;
impl Drop for EnvGuard {
fn drop(&mut self) {
std::env::remove_var("PUNKTFUNK_NVENC_SLICES");
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
remove_env("PUNKTFUNK_NVENC_SLICES");
remove_env("PUNKTFUNK_NVENC_SUBFRAME");
}
}
let _guard = EnvGuard;
pf_zerocopy::cuda::make_current().expect("shared CUDA context current");
// Escape 1: explicit single slice — no boundaries to cut, chunked poll disarmed.
std::env::set_var("PUNKTFUNK_NVENC_SLICES", "1");
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
set_env("PUNKTFUNK_NVENC_SLICES", "1");
remove_env("PUNKTFUNK_NVENC_SUBFRAME");
let mut enc = open_h265();
let frame = nv12_frame(W, H, 0);
enc.submit_indexed(&frame, 0).expect("submit");
@@ -4943,8 +4959,8 @@ mod tests {
// Escape 2: sub-frame readback vetoed — slices stay (default 4) but chunked poll
// disarms and the plain poll path carries the session.
std::env::remove_var("PUNKTFUNK_NVENC_SLICES");
std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "0");
remove_env("PUNKTFUNK_NVENC_SLICES");
set_env("PUNKTFUNK_NVENC_SUBFRAME", "0");
let mut enc = open_h265();
let frame = nv12_frame(W, H, 0);
enc.submit_indexed(&frame, 0).expect("submit");
+4 -4
View File
@@ -425,11 +425,11 @@ pub fn run_from_args(args: &[String]) -> Result<()> {
/// the priority intent (it arrives explicitly in `Hello`) and the worker path itself (nothing here
/// spawns a worker, and a stale value in a core dump is just noise).
fn sanitize_env() {
// Single-threaded — this runs before anything in this process creates a thread, which is the
// one situation where mutating the environment is sound (the `getenv` race the house rule
// about `set_var` is about needs a second thread).
for k in ["PYROWAVE_QUEUE_PRIORITY", "PUNKTFUNK_ENCODE_WORKER"] {
std::env::remove_var(k);
// SAFETY: single-threaded — this runs before anything in this process creates a thread,
// which is the one situation where mutating the environment is sound (the `getenv` race
// `remove_var`'s contract is about needs a second thread).
unsafe { std::env::remove_var(k) };
}
}
+14 -5
View File
@@ -1616,7 +1616,9 @@ impl Encoder for NvencD3d11Encoder {
let frame = match &captured.payload {
FramePayload::D3d11(f) => f,
FramePayload::Cpu(_) => {
bail!("NVENC D3D11 encoder needs a GPU texture frame (use the software encoder for CPU frames)")
bail!(
"NVENC D3D11 encoder needs a GPU texture frame (use the software encoder for CPU frames)"
)
}
};
// The capturer recreates its D3D11 device on a desktop switch (secure/Winlogon) and may come
@@ -2882,8 +2884,12 @@ mod tests {
let two = nv::NV_ENC_SPLIT_ENCODE_MODE::NV_ENC_SPLIT_TWO_FORCED_MODE as u32;
// Isolate the split variable exactly as the Linux spike does.
std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "0");
std::env::set_var("PUNKTFUNK_SPLIT_ENCODE", "0");
// SAFETY: this `#[ignore]`d hardware spike is run alone (manual RTX-box run, one test),
// so no other thread exists to read or write the environment concurrently.
unsafe {
std::env::set_var("PUNKTFUNK_NVENC_SUBFRAME", "0");
std::env::set_var("PUNKTFUNK_SPLIT_ENCODE", "0");
}
// SAFETY: (test-only) the same straight-line D3D11/DXGI setup as `nvenc_reconfigure_no_idr`.
unsafe {
@@ -3007,8 +3013,11 @@ mod tests {
enc.flush().ok();
}
std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE");
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
// SAFETY: as the set above — single-threaded manual test run, no concurrent env access.
unsafe {
std::env::remove_var("PUNKTFUNK_SPLIT_ENCODE");
std::env::remove_var("PUNKTFUNK_NVENC_SUBFRAME");
}
}
/// ON-GLASS (RTX box): the measurement gating the AYUV 4:4:4 work — encodes the probe
+1 -1
View File
@@ -6,7 +6,7 @@
[package]
name = "pf-frame"
version.workspace = true
edition = "2021"
edition.workspace = true
rust-version.workspace = true
license = "MIT OR Apache-2.0"
description = "punktfunk host shared frame/format vocabulary: CapturedFrame, PixelFormat, HDR metadata, thread QoS, and the Windows DXGI capture identity."
+4 -4
View File
@@ -21,11 +21,11 @@ mod imp {
type Bool = i32;
#[link(name = "winmm")]
extern "system" {
unsafe extern "system" {
fn timeBeginPeriod(uPeriod: u32) -> u32;
}
#[link(name = "kernel32")]
extern "system" {
unsafe extern "system" {
fn GetCurrentProcess() -> Handle;
fn SetPriorityClass(hProcess: Handle, dwPriorityClass: u32) -> Bool;
fn SetThreadExecutionState(esFlags: u32) -> u32;
@@ -46,11 +46,11 @@ mod imp {
simple_reason: *const u16,
}
#[link(name = "dwmapi")]
extern "system" {
unsafe extern "system" {
fn DwmEnableMMCSS(fEnableMMCSS: Bool) -> i32; // HRESULT
}
#[link(name = "avrt")]
extern "system" {
unsafe extern "system" {
fn AvSetMmThreadCharacteristicsW(TaskName: *const u16, TaskIndex: *mut u32) -> Handle;
}
+1 -1
View File
@@ -4,7 +4,7 @@
[package]
name = "pf-gpu"
version.workspace = true
edition = "2021"
edition.workspace = true
license = "MIT OR Apache-2.0"
description = "punktfunk host GPU vendor/adapter enumeration, selection preference, and active-session accounting."
publish = false
+28 -28
View File
@@ -169,7 +169,7 @@ mod kmt {
}
#[link(name = "gdi32")]
extern "system" {
unsafe extern "system" {
fn D3DKMTOpenAdapterFromLuid(arg: *mut OpenAdapterFromLuid) -> i32;
fn D3DKMTQueryAdapterInfo(arg: *mut QueryAdapterInfo) -> i32;
fn D3DKMTCloseAdapter(arg: *mut CloseAdapter) -> i32;
@@ -500,12 +500,12 @@ pub fn pick(
env_substr: Option<&str>,
) -> Option<(usize, PickSource)> {
let mut preference_missing = false;
if pref.mode == GpuMode::Manual {
if let Some(want) = &pref.gpu {
match find_preferred(gpus, want) {
Some(i) => return Some((i, PickSource::Preference)),
None => preference_missing = true,
}
if pref.mode == GpuMode::Manual
&& let Some(want) = &pref.gpu
{
match find_preferred(gpus, want) {
Some(i) => return Some((i, PickSource::Preference)),
None => preference_missing = true,
}
}
if let Some(sub) = env_substr.filter(|s| !s.is_empty()) {
@@ -560,17 +560,17 @@ pub fn selected_gpu() -> Option<SelectedGpu> {
let gpus = enumerate();
let pref = prefs().get();
let mut preference_missing = false;
if pref.mode == GpuMode::Manual {
if let Some(want) = &pref.gpu {
match find_preferred(&gpus, want) {
Some(i) => {
return Some(SelectedGpu {
info: gpus.into_iter().nth(i)?,
source: PickSource::Preference,
})
}
None => preference_missing = true,
if pref.mode == GpuMode::Manual
&& let Some(want) = &pref.gpu
{
match find_preferred(&gpus, want) {
Some(i) => {
return Some(SelectedGpu {
info: gpus.into_iter().nth(i)?,
source: PickSource::Preference,
});
}
None => preference_missing = true,
}
}
let source = if preference_missing {
@@ -578,13 +578,13 @@ pub fn selected_gpu() -> Option<SelectedGpu> {
} else {
PickSource::Auto
};
if linux_nvidia_present() {
if let Some(i) = gpus.iter().position(|g| g.vendor_id == VENDOR_NVIDIA) {
return Some(SelectedGpu {
info: gpus.into_iter().nth(i)?,
source,
});
}
if linux_nvidia_present()
&& let Some(i) = gpus.iter().position(|g| g.vendor_id == VENDOR_NVIDIA)
{
return Some(SelectedGpu {
info: gpus.into_iter().nth(i)?,
source,
});
}
let node = linux_render_node();
let i = gpus
@@ -620,10 +620,10 @@ pub fn manual_selection() -> Option<GpuInfo> {
/// (a deliberate live env read — see `config.rs` module docs) > `/dev/dri/renderD128`.
#[cfg(target_os = "linux")]
pub fn linux_render_node() -> PathBuf {
if let Some(g) = manual_selection() {
if let Some(node) = g.handle.render_node {
return node;
}
if let Some(g) = manual_selection()
&& let Some(node) = g.handle.render_node
{
return node;
}
std::env::var("PUNKTFUNK_RENDER_NODE")
.ok()
+1 -1
View File
@@ -4,7 +4,7 @@
[package]
name = "pf-host-config"
version.workspace = true
edition = "2021"
edition.workspace = true
license = "MIT OR Apache-2.0"
description = "Process-wide punktfunk host configuration (env-parsed HostConfig behind a OnceLock)."
publish = false
+10 -10
View File
@@ -84,17 +84,17 @@ impl AudioOutputMode {
/// first (it is the more restrictive promise — "do not touch my devices" must not be overridden
/// by a stale `PUNKTFUNK_HOST_AUDIO` in the same `host.env`).
fn from_env() -> AudioOutputMode {
if let Ok(raw) = std::env::var("PUNKTFUNK_AUDIO_OUTPUT_MODE") {
if !raw.trim().is_empty() {
if let Some(m) = AudioOutputMode::parse(&raw) {
return m;
}
// Never silently fall through to a different routing than the operator asked for.
eprintln!(
"punktfunk: PUNKTFUNK_AUDIO_OUTPUT_MODE={raw:?} is not one of \
client_only/host_and_client/follow_default using client_only"
);
if let Ok(raw) = std::env::var("PUNKTFUNK_AUDIO_OUTPUT_MODE")
&& !raw.trim().is_empty()
{
if let Some(m) = AudioOutputMode::parse(&raw) {
return m;
}
// Never silently fall through to a different routing than the operator asked for.
eprintln!(
"punktfunk: PUNKTFUNK_AUDIO_OUTPUT_MODE={raw:?} is not one of \
client_only/host_and_client/follow_default using client_only"
);
}
if std::env::var_os("PUNKTFUNK_KEEP_DEFAULT").is_some() {
return AudioOutputMode::FollowDefault;
+1 -1
View File
@@ -7,7 +7,7 @@
[package]
name = "pf-inject"
version.workspace = true
edition = "2021"
edition.workspace = true
rust-version.workspace = true
license = "MIT OR Apache-2.0"
description = "punktfunk host input injection: per-OS keyboard/mouse injectors + the virtual-gamepad HID backends behind one InputInjector trait."
+1 -1
View File
@@ -4,7 +4,7 @@
[package]
name = "pf-paths"
version.workspace = true
edition = "2021"
edition.workspace = true
license = "MIT OR Apache-2.0"
description = "Host config-directory resolution + owner-private file/dir creation (0600/0700 or SYSTEM/Admins DACL)."
publish = false
+4 -1
View File
@@ -398,7 +398,10 @@ mod linux_main {
}
// One libc symbol, declared directly — not worth a libc dependency in a root helper.
extern "C" {
// (Edition 2024 spells extern blocks `unsafe extern`, which the `unsafe_code` lint now
// counts — the same one-named-exemption rule as `effective_uid` below applies.)
#[allow(unsafe_code)]
unsafe extern "C" {
#[link_name = "geteuid"]
fn libc_geteuid() -> u32;
}
+1 -1
View File
@@ -8,7 +8,7 @@
[package]
name = "pf-vdisplay"
version.workspace = true
edition = "2021"
edition.workspace = true
rust-version.workspace = true
license = "MIT OR Apache-2.0"
description = "punktfunk host virtual-display orchestration: per-compositor Linux backends + the Windows IddCx driver backend behind one VirtualDisplay trait."
+130 -118
View File
@@ -16,7 +16,7 @@
//! lands — a runtime gate on `remote_fd.is_some()`.
//!
//! The ownership split: the session's capturer no longer owns the real keepalive — the registry does.
//! [`acquire`] hands the session a `VirtualOutput` whose `keepalive` is a lightweight, gen-stamped
//! [`acquire`] hands the session a `VirtualOutput` whose `keepalive` is a lightweight, generation-stamped
//! `DisplayLease` (mirrors the Windows `MonitorLease`); dropping it releases the registry refcount,
//! and the lifecycle machine decides linger / teardown. `capture_virtual_output`'s signature is
//! unchanged — it just holds a lease instead of the real keepalive.
@@ -85,7 +85,7 @@ fn topology_str() -> String {
/// with the quit application code — a user "stop", not a network drop), the display is torn down
/// **immediately**, skipping the keep-alive linger. A bare disconnect leaves it `false` → normal linger.
///
/// `supersedes`: the pool gen of a display this acquire REPLACES (a mid-stream mode switch creates
/// `supersedes`: the pool generation of a display this acquire REPLACES (a mid-stream mode switch creates
/// the new display before retiring the old — create-before-drop). The replacement inherits group
/// topology ownership: without this, the dying predecessor counts as a live sibling and the new
/// display "extends" behind it, losing a Primary/Exclusive topology on every resize. `None`
@@ -151,7 +151,7 @@ pub fn snapshot() -> Snapshot {
.into_iter()
.enumerate()
.map(|(idx, i)| DisplayInfo {
slot: i.gen,
slot: i.generation,
backend: i.backend.to_string(),
mode: i.mode,
state: i.state.to_string(),
@@ -185,7 +185,7 @@ pub fn snapshot() -> Snapshot {
/// released.
pub fn release(slot: Option<u64>) -> usize {
#[cfg(target_os = "windows")]
// Windows slots (Stage W1): `slot` selects one kept monitor by its gen stamp
// Windows slots (Stage W1): `slot` selects one kept monitor by its generation stamp
// ([`DisplayInfo::slot`]); `None` releases every kept one.
let released = super::manager::force_release(slot);
#[cfg(target_os = "linux")]
@@ -211,12 +211,12 @@ pub fn release(slot: Option<u64>) -> usize {
/// Tear down a **reused-but-dead** pool entry by its generation stamp (A2). Called by the pipeline
/// builder when the first frame fails on a display [`acquire`] handed back as REUSED — so the retry
/// loop's next `acquire` creates fresh instead of re-wedging on the same corpse. No-op off Linux / if
/// the entry is already gone (idempotent — the subsequent stale-gen lease drop no-ops too).
pub fn mark_failed(gen: u64) {
/// the entry is already gone (idempotent — the subsequent stale-generation lease drop no-ops too).
pub fn mark_failed(generation: u64) {
#[cfg(target_os = "linux")]
linux::mark_failed(gen);
linux::mark_failed(generation);
#[cfg(not(target_os = "linux"))]
let _ = gen;
let _ = generation;
}
/// Force-release a **superseded** kept display by its generation stamp
@@ -225,13 +225,13 @@ pub fn mark_failed(gen: u64) {
/// keep-alive policy every resize would accumulate kept monitors at stale modes. The mode-switch
/// arm calls this once the new pipeline is up and the old capturer is dropped. Only a KEPT
/// (lingering/pinned) entry is released — an Active one is refused, like `/display/release` — and
/// a gen that's already gone (immediate teardown) is a no-op. No-op off Linux (Windows
/// a generation that's already gone (immediate teardown) is a no-op. No-op off Linux (Windows
/// reconfigures the same monitor in place — nothing is superseded).
pub fn retire(gen: u64) {
pub fn retire(generation: u64) {
#[cfg(target_os = "linux")]
linux::retire(gen);
linux::retire(generation);
#[cfg(not(target_os = "linux"))]
let _ = gen;
let _ = generation;
}
/// Invalidate every kept display of `backend` — its compositor instance is gone (a Game↔Desktop switch
@@ -320,9 +320,9 @@ mod pool {
/// The session epoch at creation (A4). Reuse requires an epoch match; the linger timer reaps
/// entries whose epoch is stale (their compositor instance was replaced under them).
pub(super) epoch: u64,
/// Generation stamp: a `DisplayLease` only releases if its gen still matches (a stale lease
/// Generation stamp: a `DisplayLease` only releases if its generation still matches (a stale lease
/// — its entry was reused + re-stamped — is a no-op).
pub(super) gen: u64,
pub(super) generation: u64,
/// The out-of-band-cursor mode this display was CREATED with (Phase B): metadata-pointer
/// (cursor-channel session) vs compositor-embedded. Reuse requires an exact match — a kept
/// embedded display has no cursor metadata for a channel session to forward, and a kept
@@ -344,15 +344,15 @@ mod pool {
/// gamescope **spawn** is an independent nested session per client (no shared desktop), so each
/// gamescope display is its OWN group — never auto-rowed against, or topology-/restore-grouped with,
/// another gamescope session.
pub(super) fn group_key(backend: &str, gen: u64) -> String {
pub(super) fn group_key(backend: &str, generation: u64) -> String {
if backend == "gamescope" {
format!("gamescope#{gen}")
format!("gamescope#{generation}")
} else {
backend.to_string()
}
}
/// Is the pooled entry `(e_backend, e_gen)` a member of the group the display `(backend, gen)`
/// Is the pooled entry `(e_backend, e_gen)` a member of the group the display `(backend, generation)`
/// belongs to — the ONE definition of membership, shared by the restore hand-off, the
/// first-in-group probe and the layout collection.
///
@@ -370,10 +370,10 @@ mod pool {
e_backend: &str,
e_gen: u64,
backend: &str,
gen: u64,
generation: u64,
supersedes: Option<u64>,
) -> bool {
Some(e_gen) != supersedes && group_key(e_backend, e_gen) == group_key(backend, gen)
Some(e_gen) != supersedes && group_key(e_backend, e_gen) == group_key(backend, generation)
}
/// Hand off a torn-down display's topology restore (design §6.1 — per-group restore): if a
@@ -382,20 +382,20 @@ mod pool {
/// reclaimed display's keepalive, so the physical is re-enabled while our output still exists —
/// the compositor never sees zero outputs). `None` in → `None` out.
///
/// `backend`+`gen` identify the DEPARTING display, and both are needed: keyed on the backend name
/// `backend`+`generation` identify the DEPARTING display, and both are needed: keyed on the backend name
/// alone, one gamescope spawn's restore floated onto an unrelated client's spawn — where it would
/// run when THAT session ended and never when its own did.
pub(super) fn hand_off_restore(
remaining: &mut [Entry],
backend: &'static str,
gen: u64,
generation: u64,
restore: Option<Restore>,
) -> Option<Restore> {
let action = restore?;
// At most one restore per group, so any surviving sibling has `None` to receive it.
match remaining
.iter_mut()
.find(|e| in_group(e.backend, e.gen, backend, gen, None))
.find(|e| in_group(e.backend, e.generation, backend, generation, None))
{
Some(sibling) => {
sibling.topology_restore = Some(action);
@@ -441,8 +441,9 @@ mod pool {
&& !matches!(entries[i].life, lifecycle::State::Active { .. });
if entries[i].life.poll_expiry(now) || dead_epoch {
let mut e = entries.remove(i);
let (backend, gen) = (e.backend, e.gen);
if let Some(r) = hand_off_restore(entries, backend, gen, e.topology_restore.take())
let (backend, generation) = (e.backend, e.generation);
if let Some(r) =
hand_off_restore(entries, backend, generation, e.topology_restore.take())
{
restores.push(r);
}
@@ -470,7 +471,7 @@ mod pool {
/// One live/kept display, flattened out of the pool under the lock — so the group + arrangement
/// math (which calls the layout engine) runs OUTSIDE the lock.
pub(super) struct Row {
pub(super) gen: u64,
pub(super) generation: u64,
pub(super) backend: &'static str,
pub(super) mode: Mode,
pub(super) identity_slot: Option<u32>,
@@ -480,7 +481,7 @@ mod pool {
}
/// The desktop position for a display just appended to its group (design §6.2): the group's
/// `existing` members (each with its acquire `gen`) plus `new` last, ordered by `gen`, arranged by
/// `existing` members (each with its acquire `generation`) plus `new` last, ordered by `generation`, arranged by
/// the pure [`layout`](crate::layout) engine, taking the new member's placement. Pure — so the
/// append-in-acquire-order + auto-row/manual arrangement is unit-tested independent of the
/// pool/global.
@@ -503,10 +504,10 @@ mod pool {
/// is dropped. Pure over its `known`/`next` state — the caller owns the process-lifetime copy.
///
/// Ids used to be the index into the sorted key list, which meant a new group could RENUMBER an
/// untouched one: with one KWin desktop at group 1, a gamescope spawn at gen 3 sorts ahead of
/// untouched one: with one KWin desktop at group 1, a gamescope spawn at generation 3 sorts ahead of
/// `"kwin"` and silently moved the unchanged desktop to group 2 on the next `/display/state` poll.
/// A monotonic counter, remembered per key, cannot do that. Pruning to the live keys is what keeps
/// the map bounded: the per-spawn keys (`gamescope#<gen>`, one per dedicated session) would
/// the map bounded: the per-spawn keys (`gamescope#<generation>`, one per dedicated session) would
/// otherwise accumulate for the host's lifetime — an id is retired with its group.
pub(super) fn assign_group_ids(
known: &mut std::collections::BTreeMap<String, u32>,
@@ -523,7 +524,7 @@ mod pool {
}
/// Group the flattened rows into the mgmt `/display/state` view (design §6.1/§6.2) by
/// [`group_key`], ordered by acquire (`gen`), with each member's position from the pure
/// [`group_key`], ordered by acquire (`generation`), with each member's position from the pure
/// [`layout`](crate::layout) engine. `ids` maps each group key to its reported group id (see
/// [`group_ids`]). Pure — no I/O, no global — so the grouping / ordering / position assignment is
/// unit-tested against synthetic rows.
@@ -535,20 +536,23 @@ mod pool {
) -> Vec<DisplayInfo> {
use crate::layout::{self, Member};
let mut keys: Vec<String> = rows.iter().map(|r| group_key(r.backend, r.gen)).collect();
let mut keys: Vec<String> = rows
.iter()
.map(|r| group_key(r.backend, r.generation))
.collect();
keys.sort();
keys.dedup();
let mut out: Vec<DisplayInfo> = Vec::new();
for key in keys.iter() {
// This group's members in acquire order (gen ascending) → display_index + arrangement.
// This group's members in acquire order (generation ascending) → display_index + arrangement.
let mut idx: Vec<usize> = rows
.iter()
.enumerate()
.filter(|(_, row)| &group_key(row.backend, row.gen) == key)
.filter(|(_, row)| &group_key(row.backend, row.generation) == key)
.map(|(i, _)| i)
.collect();
idx.sort_by_key(|&i| rows[i].gen);
idx.sort_by_key(|&i| rows[i].generation);
let members: Vec<Member> = idx
.iter()
.map(|&i| Member {
@@ -561,7 +565,7 @@ mod pool {
let row = &rows[i];
let p = places[ord];
out.push(DisplayInfo {
slot: row.gen,
slot: row.generation,
backend: row.backend.to_string(),
mode: (row.mode.width, row.mode.height, row.mode.refresh_hz),
state: row.state.to_string(),
@@ -590,8 +594,8 @@ mod pool {
use std::sync::Arc;
/// A minimal pool entry for the pure teardown/restore tests (dummy keepalive; the
/// `hand_off_restore` logic only reads `backend` + `gen` + `topology_restore`).
fn test_entry(backend: &'static str, gen: u64, restore: Option<Restore>) -> Entry {
/// `hand_off_restore` logic only reads `backend` + `generation` + `topology_restore`).
fn test_entry(backend: &'static str, generation: u64, restore: Option<Restore>) -> Entry {
Entry {
life: lifecycle::State::default(),
keepalive: Box::new(()),
@@ -607,7 +611,7 @@ mod pool {
topology_restore: restore,
launch: None,
epoch: 0,
gen,
generation,
hw_cursor: false,
hdr: false,
}
@@ -631,7 +635,10 @@ mod pool {
/// `ids_for` against a CARRIED map — for the stability test, which needs the same state
/// across two assemblies.
fn ids_into(known: &mut BTreeMap<String, u32>, next: &mut u32, rows: &[Row]) {
let mut keys: Vec<String> = rows.iter().map(|r| group_key(r.backend, r.gen)).collect();
let mut keys: Vec<String> = rows
.iter()
.map(|r| group_key(r.backend, r.generation))
.collect();
keys.sort();
keys.dedup();
assign_group_ids(known, next, &keys);
@@ -659,7 +666,7 @@ mod pool {
#[test]
fn topology_restore_floats_to_a_sibling_then_runs_on_the_last_teardown() {
let ran = Arc::new(AtomicBool::new(false));
// Two KWin displays in one group; the first (gen 1) carries the group's restore.
// Two KWin displays in one group; the first (generation 1) carries the group's restore.
let mut pool = vec![
test_entry("kwin", 1, Some(flag_restore(&ran))),
test_entry("kwin", 2, None),
@@ -697,7 +704,7 @@ mod pool {
#[test]
fn tearing_down_a_non_carrier_first_leaves_the_restore_for_last() {
let ran = Arc::new(AtomicBool::new(false));
// gen 2 carries the restore; gen 1 does not (a later exclusive session found the physical
// generation 2 carries the restore; generation 1 does not (a later exclusive session found the physical
// already disabled).
let mut pool = vec![
test_entry("kwin", 1, None),
@@ -706,7 +713,7 @@ mod pool {
// Tear down the non-carrier first → nothing to hand off, carrier untouched.
let mut e1 = pool.remove(0);
assert!(hand_off_restore(&mut pool, "kwin", 1, e1.topology_restore.take()).is_none());
// The carrier (gen 2) still holds the group's restore.
// The carrier (generation 2) still holds the group's restore.
assert!(pool[0].topology_restore.is_some());
// Now the carrier (last member) → run.
let mut e2 = pool.remove(0);
@@ -755,9 +762,9 @@ mod pool {
assert!(in_group("kwin", 3, "kwin", 2, Some(1)));
}
fn row(gen: u64, backend: &'static str, w: u32, slot: Option<u32>) -> Row {
fn row(generation: u64, backend: &'static str, w: u32, slot: Option<u32>) -> Row {
Row {
gen,
generation,
backend,
mode: Mode {
width: w,
@@ -773,7 +780,7 @@ mod pool {
#[test]
fn groups_by_backend_and_auto_rows_in_acquire_order() {
// Two KWin displays (acquired gen 5 then gen 2 — deliberately out of vec order) + a Mutter one.
// Two KWin displays (acquired generation 5 then generation 2 — deliberately out of vec order) + a Mutter one.
let rows = vec![
row(5, "kwin", 2560, Some(1)),
row(2, "kwin", 1920, Some(7)),
@@ -784,12 +791,12 @@ mod pool {
let kwin: Vec<&DisplayInfo> = out.iter().filter(|d| d.backend == "kwin").collect();
assert_eq!(kwin.len(), 2);
assert_eq!(kwin[0].slot, 2); // lower gen (earlier acquire) sorts to index 0
assert_eq!(kwin[0].slot, 2); // lower generation (earlier acquire) sorts to index 0
assert_eq!(kwin[0].display_index, 0);
assert_eq!(kwin[0].position, (0, 0));
assert_eq!(kwin[1].slot, 5);
assert_eq!(kwin[1].display_index, 1);
assert_eq!(kwin[1].position, (1920, 0)); // auto-row: after the 1920px gen-2 display
assert_eq!(kwin[1].position, (1920, 0)); // auto-row: after the 1920px generation-2 display
assert_eq!(kwin[0].topology, "exclusive");
// A distinct backend is a distinct group.
@@ -830,7 +837,7 @@ mod pool {
identity_slot: slot,
width: w,
};
// Existing group (given out of gen order): gen 8 @ 1920 acquired AFTER gen 3 @ 2560.
// Existing group (given out of generation order): generation 8 @ 1920 acquired AFTER generation 3 @ 2560.
let existing = vec![(8, m(Some(2), 1920)), (3, m(Some(1), 2560))];
// A new 1280-wide display appends to the right of 2560 + 1920.
let pos = position_for_new(existing, m(Some(5), 1280), &Layout::default());
@@ -900,30 +907,30 @@ mod pool {
use std::time::Duration;
let t0 = Instant::now();
let mut es = Vec::new();
// gen 1: lingering, deadline passed.
// generation 1: lingering, deadline passed.
let mut e1 = test_entry("kwin", 1, None);
e1.life = lifecycle::State::Lingering {
until: t0 - Duration::from_millis(1),
};
es.push(e1);
// gen 2: lingering, deadline in the future, current epoch → survives.
// generation 2: lingering, deadline in the future, current epoch → survives.
let mut e2 = test_entry("kwin", 2, None);
e2.life = lifecycle::State::Lingering {
until: t0 + Duration::from_secs(60),
};
e2.epoch = 5;
es.push(e2);
// gen 3: pinned, but from a DEAD epoch → reaped (its compositor is gone).
// generation 3: pinned, but from a DEAD epoch → reaped (its compositor is gone).
let mut e3 = test_entry("kwin", 3, None);
e3.life = lifecycle::State::Pinned;
e3.epoch = 4;
es.push(e3);
// gen 4: ACTIVE from a dead epoch → left to its own session's rebuild.
// generation 4: ACTIVE from a dead epoch → left to its own session's rebuild.
let mut e4 = test_entry("kwin", 4, None);
e4.life = lifecycle::State::Active { refs: 1 };
e4.epoch = 4;
es.push(e4);
// gen 5: a gamescope spawn from a "dead" epoch — exempt (independent nested session).
// generation 5: a gamescope spawn from a "dead" epoch — exempt (independent nested session).
let mut e5 = test_entry("gamescope", 5, None);
e5.life = lifecycle::State::Pinned;
e5.epoch = 1;
@@ -931,9 +938,9 @@ mod pool {
let (expired, restores) = take_expired(&mut es, t0, 5);
assert!(restores.is_empty());
let gone: Vec<u64> = expired.iter().map(|e| e.gen).collect();
let gone: Vec<u64> = expired.iter().map(|e| e.generation).collect();
assert_eq!(gone, vec![1, 3]);
let left: Vec<u64> = es.iter().map(|e| e.gen).collect();
let left: Vec<u64> = es.iter().map(|e| e.generation).collect();
assert_eq!(left, vec![2, 4, 5]);
}
}
@@ -974,7 +981,7 @@ mod linux {
struct Reg {
entries: Mutex<Vec<Entry>>,
gen: AtomicU64,
generation: AtomicU64,
}
static REG: OnceLock<Reg> = OnceLock::new();
@@ -982,7 +989,7 @@ mod linux {
fn reg() -> &'static Reg {
REG.get_or_init(|| Reg {
entries: Mutex::new(Vec::new()),
gen: AtomicU64::new(1),
generation: AtomicU64::new(1),
})
}
@@ -1029,25 +1036,27 @@ mod linux {
}
match std::thread::Builder::new()
.name("vdisplay-linger".into())
.spawn(|| loop {
std::thread::sleep(Duration::from_millis(500));
let (expired, restores) = {
let mut es = reg().entries.lock().unwrap();
take_expired(&mut es, Instant::now(), crate::session_epoch())
};
// Re-enable physicals (group emptied) BEFORE dropping the outputs — outside the lock.
for restore in restores {
restore();
.spawn(|| {
loop {
std::thread::sleep(Duration::from_millis(500));
let (expired, restores) = {
let mut es = reg().entries.lock().unwrap();
take_expired(&mut es, Instant::now(), crate::session_epoch())
};
// Re-enable physicals (group emptied) BEFORE dropping the outputs — outside the lock.
for restore in restores {
restore();
}
let reaped = expired.len();
for e in expired {
tracing::info!(
backend = e.backend,
"virtual display: linger expired — torn down"
);
drop(e); // outside the lock
}
emit_released(reaped);
}
let reaped = expired.len();
for e in expired {
tracing::info!(
backend = e.backend,
"virtual display: linger expired — torn down"
);
drop(e); // outside the lock
}
emit_released(reaped);
}) {
Ok(_) => *started = true,
Err(e) => tracing::error!(
@@ -1069,27 +1078,27 @@ mod linux {
}
}
/// Build the session-facing [`VirtualOutput`]: the kept node + a fresh gen-stamped lease. Only
/// Build the session-facing [`VirtualOutput`]: the kept node + a fresh generation-stamped lease. Only
/// the poolable (`remote_fd == None`) backends reach here, so `remote_fd` is always `None`.
fn output_for(
node_id: u32,
preferred_mode: Option<(u32, u32, u32)>,
gen: u64,
generation: u64,
quit: Arc<AtomicBool>,
reused: bool,
) -> VirtualOutput {
// The pooled display is registry-owned; the session holds a gen-stamped lease as its keepalive.
// The pooled display is registry-owned; the session holds a generation-stamped lease as its keepalive.
let mut out = VirtualOutput::owned(
node_id,
preferred_mode,
Box::new(DisplayLease { gen, quit }),
Box::new(DisplayLease { generation, quit }),
);
// A2: tell the pipeline builder this was a REUSED kept display, so a first-frame failure can
// `mark_failed(gen)` (tear the corpse down) rather than re-wedge the retry loop on the same node.
out.reused_gen = reused.then_some(gen);
// H4: every pooled display carries its gen, so a mode-switch rebuild can `retire` the entry
// `mark_failed(generation)` (tear the corpse down) rather than re-wedge the retry loop on the same node.
out.reused_gen = reused.then_some(generation);
// H4: every pooled display carries its generation, so a mode-switch rebuild can `retire` the entry
// this output's successor supersedes.
out.pool_gen = Some(gen);
out.pool_gen = Some(generation);
out
}
@@ -1129,9 +1138,9 @@ mod linux {
// gamescope spawns are independent nested sessions, exempt from the active-session epoch —
// see `epoch_matches`). The liveness probe (`kept_display_alive`, which may shell `pw-dump`
// for gamescope) must NOT run under the pool lock (it can block / hang the daemon), so:
// 1. find the candidate + snapshot (gen, node_id) UNDER the lock, then release it;
// 1. find the candidate + snapshot (generation, node_id) UNDER the lock, then release it;
// 2. probe liveness OUTSIDE the lock;
// 3. re-lock and re-find the SAME entry by its gen (another thread may have reused/removed
// 3. re-lock and re-find the SAME entry by its generation (another thread may have reused/removed
// it meanwhile — then we just miss and create fresh).
let candidate = {
let es = r.entries.lock().unwrap();
@@ -1147,16 +1156,16 @@ mod linux {
&& e.hdr == vd.hdr()
&& epoch_matches(e.backend, e.epoch, cur_epoch)
})
.map(|e| (e.gen, e.node_id))
.map(|e| (e.generation, e.node_id))
};
if let Some((cand_gen, node_id)) = candidate {
let alive = vd.kept_display_alive(node_id); // OUTSIDE the lock (may block)
let reuse = {
let mut es = r.entries.lock().unwrap();
// Re-find the SAME entry by its snapshot gen; skip if it's gone or no longer kept
// Re-find the SAME entry by its snapshot generation; skip if it's gone or no longer kept
// (a concurrent reconnect adopted it) — we then miss and create fresh.
match es.iter().position(|e| {
e.gen == cand_gen
e.generation == cand_gen
&& matches!(
e.life,
lifecycle::State::Lingering { .. } | lifecycle::State::Pinned
@@ -1164,8 +1173,8 @@ mod linux {
}) {
Some(idx) if alive => {
es[idx].life.acquire();
let gen = r.gen.fetch_add(1, Ordering::Relaxed);
es[idx].gen = gen;
let generation = r.generation.fetch_add(1, Ordering::Relaxed);
es[idx].generation = generation;
let preferred_mode = es[idx].preferred_mode;
tracing::info!(
backend,
@@ -1175,7 +1184,7 @@ mod linux {
ReuseOutcome::Reused(output_for(
node_id,
preferred_mode,
gen,
generation,
quit.clone(),
true,
))
@@ -1183,7 +1192,7 @@ mod linux {
Some(idx) => {
// Dead kept display: remove it, hand off its group restore, create fresh.
let mut dead = es.remove(idx);
let (b, g) = (dead.backend, dead.gen);
let (b, g) = (dead.backend, dead.generation);
let restore =
hand_off_restore(&mut es, b, g, dead.topology_restore.take());
ReuseOutcome::Dead(dead, restore)
@@ -1213,9 +1222,9 @@ mod linux {
// The new display's generation stamp, taken BEFORE the group questions below: it is this
// display's identity for the rest of the acquire, and `group_key` needs it — a gamescope
// spawn's group IS its gen. A gen burned by a failed create is harmless (they are opaque
// spawn's group IS its generation. A generation burned by a failed create is harmless (they are opaque
// and monotonic, never an index).
let gen = r.gen.fetch_add(1, Ordering::Relaxed);
let generation = r.generation.fetch_add(1, Ordering::Relaxed);
// NOTE: the operator's `max_displays` ceiling is NOT enforced here. It belongs at
// admission (`admission::admit`), which is where Windows has always applied it, and the
@@ -1240,7 +1249,7 @@ mod linux {
let first_in_group = {
let es = r.entries.lock().unwrap();
!es.iter().any(|e| {
in_group(e.backend, e.gen, backend, gen, supersedes)
in_group(e.backend, e.generation, backend, generation, supersedes)
&& matches!(e.life, lifecycle::State::Active { .. })
})
};
@@ -1292,7 +1301,7 @@ mod linux {
topology_restore,
launch: launch.clone(),
epoch: cur_epoch,
gen,
generation,
hw_cursor: vd.hw_cursor(),
hdr: vd.hdr(),
};
@@ -1314,10 +1323,10 @@ mod linux {
// width to the right on every mode switch.
let existing: Vec<(u64, Member)> = es
.iter()
.filter(|e| in_group(e.backend, e.gen, backend, gen, supersedes))
.filter(|e| in_group(e.backend, e.generation, backend, generation, supersedes))
.map(|e| {
(
e.gen,
e.generation,
Member {
identity_slot: e.identity_slot,
width: e.mode.width as i32,
@@ -1340,7 +1349,7 @@ mod linux {
if (position.x, position.y) != (0, 0) {
vd.apply_position(position.x, position.y);
}
let mut out = output_for(node_id, preferred_mode, gen, quit, false);
let mut out = output_for(node_id, preferred_mode, generation, quit, false);
out.expect_exact_dims = expect_exact_dims;
Ok(out)
}
@@ -1348,18 +1357,18 @@ mod linux {
/// The [`DisplayLease`] `Drop` path: release the session's hold on the pooled display. The
/// lifecycle machine decides linger / pin / teardown; a torn-down entry's keepalive drops *after*
/// the lock is released.
fn release(gen: u64, force_immediate: bool) {
fn release(generation: u64, force_immediate: bool) {
let Some(r) = REG.get() else { return };
let linger = effective_linger(force_immediate, linger());
let (torn_down, restore) = {
let mut es = r.entries.lock().unwrap();
let Some(idx) = es.iter().position(|e| e.gen == gen) else {
let Some(idx) = es.iter().position(|e| e.generation == generation) else {
return; // stale lease (entry reused + re-stamped, or already gone) — no-op
};
match es[idx].life.release(Instant::now(), linger) {
Release::Teardown => {
let mut e = es.remove(idx);
let (backend, g) = (e.backend, e.gen);
let (backend, g) = (e.backend, e.generation);
// Per-group restore (§6.1): hand the physical re-enable to a surviving sibling, or run
// it now if this was the group's last member.
let restore = hand_off_restore(&mut es, backend, g, e.topology_restore.take());
@@ -1368,8 +1377,8 @@ mod linux {
// A release against a slot with NO live hold — a stale or duplicate lease drop. The
// machine's contract for it is "do nothing", and it was wired to the teardown arm:
// the one outcome that means the caller has no claim on this display would have torn
// the display down. Unreachable today (a lease's gen is unique per acquire and the
// lookup above is by gen, so a stale lease misses the pool entirely and returns
// the display down. Unreachable today (a lease's generation is unique per acquire and the
// lookup above is by generation, so a stale lease misses the pool entirely and returns
// early), which is exactly why it has to be right by construction rather than by
// luck — matching the Windows manager's own Noop arm.
Release::Noop => (None, None),
@@ -1434,7 +1443,7 @@ mod linux {
lifecycle::State::Idle => return None,
};
Some(Row {
gen: e.gen,
generation: e.generation,
backend: e.backend,
mode: e.mode,
identity_slot: e.identity_slot,
@@ -1455,7 +1464,10 @@ mod linux {
// Stable per-group ids, carried across polls (see `assign_group_ids`) — a new group must
// never renumber an existing one under the console. The state is process-lifetime and this
// is the only thing that touches it, so it lives here rather than in the pure core.
let mut keys: Vec<String> = rows.iter().map(|r| group_key(r.backend, r.gen)).collect();
let mut keys: Vec<String> = rows
.iter()
.map(|r| group_key(r.backend, r.generation))
.collect();
keys.sort();
keys.dedup();
static GROUP_IDS: Mutex<Option<(std::collections::BTreeMap<String, u32>, u32)>> =
@@ -1475,13 +1487,13 @@ mod linux {
}
/// H4 — force-release a display superseded by a mid-stream mode switch. Same machinery as
/// [`force_release`] (kept entries only — an Active entry is refused, and a gen already torn
/// [`force_release`] (kept entries only — an Active entry is refused, and a generation already torn
/// down under `immediate` is a no-op), distinct log line.
pub(super) fn retire(gen: u64) {
release_kept(Some(gen), "retired (superseded by a mode switch)");
pub(super) fn retire(generation: u64) {
release_kept(Some(generation), "retired (superseded by a mode switch)");
}
/// Remove + tear down KEPT (lingering/pinned) entries — all of them, or one by gen — running /
/// Remove + tear down KEPT (lingering/pinned) entries — all of them, or one by generation — running /
/// handing off group topology restores, with keepalive drops outside the lock. The shared core
/// of [`force_release`] (mgmt) and [`retire`] (mode-switch supersede).
fn release_kept(slot: Option<u64>, why: &'static str) -> usize {
@@ -1492,10 +1504,10 @@ mod linux {
let mut restores = Vec::new();
let mut i = 0;
while i < es.len() {
let selected = slot.is_none_or(|s| es[i].gen == s);
let selected = slot.is_none_or(|s| es[i].generation == s);
if selected && es[i].life.force_release() {
let mut e = es.remove(i);
let (backend, g) = (e.backend, e.gen);
let (backend, g) = (e.backend, e.generation);
let restore = e.topology_restore.take();
if let Some(rst) = hand_off_restore(&mut es, backend, g, restore) {
restores.push(rst);
@@ -1522,15 +1534,15 @@ mod linux {
/// A2 — tear down a reused-but-dead pool entry by its generation stamp. Removes it (hand off /
/// run its group restore), drops the keepalive outside the lock. Idempotent (already gone → no-op).
pub(super) fn mark_failed(gen: u64) {
pub(super) fn mark_failed(generation: u64) {
let Some(r) = REG.get() else { return };
let (torn, restore) = {
let mut es = r.entries.lock().unwrap();
let Some(idx) = es.iter().position(|e| e.gen == gen) else {
return; // already gone — the subsequent stale-gen lease drop no-ops too
let Some(idx) = es.iter().position(|e| e.generation == generation) else {
return; // already gone — the subsequent stale-generation lease drop no-ops too
};
let mut e = es.remove(idx);
let (backend, g) = (e.backend, e.gen);
let (backend, g) = (e.backend, e.generation);
let restore = hand_off_restore(&mut es, backend, g, e.topology_restore.take());
(e, restore)
};
@@ -1559,7 +1571,7 @@ mod linux {
while i < es.len() {
if es[i].backend == backend {
let mut e = es.remove(i);
let (b, g) = (e.backend, e.gen);
let (b, g) = (e.backend, e.generation);
if let Some(rst) = hand_off_restore(&mut es, b, g, e.topology_restore.take()) {
restores.push(rst);
}
@@ -1591,7 +1603,7 @@ mod linux {
/// The session's refcount handle — the `keepalive` the capturer holds. `Drop` releases the
/// registry hold; a stale lease (its entry was reused + re-stamped, or torn down) is a no-op.
struct DisplayLease {
gen: u64,
generation: u64,
/// The session's deliberate-quit flag: set when the client closes with the quit application
/// code (a user "stop", not a network drop), so this lease's `Drop` tears the display down
/// immediately instead of lingering. `false` on a bare disconnect → normal keep-alive.
@@ -1600,7 +1612,7 @@ mod linux {
impl Drop for DisplayLease {
fn drop(&mut self) {
release(self.gen, self.quit.load(Ordering::SeqCst));
release(self.generation, self.quit.load(Ordering::SeqCst));
}
}
}
+11 -4
View File
@@ -161,7 +161,10 @@ pub fn apply_input_env(chosen: Compositor, dedicated_launch: bool) -> Option<Gam
// injector as sway/river, no code change.
Compositor::Wlroots | Compositor::Hyprland => "wlr",
};
std::env::set_var("PUNKTFUNK_INPUT_BACKEND", backend);
// SAFETY: `_env_guard` holds [`ENV_LOCK`] — the crate-wide discipline (lib.rs) serializing
// every process-env writer on the session-setup path; steady-state threads read cached
// config, not the environment.
unsafe { std::env::set_var("PUNKTFUNK_INPUT_BACKEND", backend) };
drop(_env_guard);
resolve_gamescope_route(chosen, dedicated_launch)
}
@@ -464,11 +467,15 @@ mod tests {
use super::operator_gamescope;
let first = operator_gamescope();
let restore = crate::with_env_lock(|| std::env::var_os("PUNKTFUNK_GAMESCOPE_NODE"));
crate::with_env_lock(|| std::env::set_var("PUNKTFUNK_GAMESCOPE_NODE", "auto"));
// SAFETY: both mutations run under `with_env_lock` — the crate's env-writer
// serialization (ENV_LOCK, lib.rs); the readers under test sample once at startup.
crate::with_env_lock(|| unsafe { std::env::set_var("PUNKTFUNK_GAMESCOPE_NODE", "auto") });
let second = operator_gamescope();
crate::with_env_lock(|| match &restore {
Some(v) => std::env::set_var("PUNKTFUNK_GAMESCOPE_NODE", v),
None => std::env::remove_var("PUNKTFUNK_GAMESCOPE_NODE"),
// SAFETY: as above — the restore also runs under the same env-writer lock.
Some(v) => unsafe { std::env::set_var("PUNKTFUNK_GAMESCOPE_NODE", v) },
// SAFETY: as above.
None => unsafe { std::env::remove_var("PUNKTFUNK_GAMESCOPE_NODE") },
});
assert_eq!(
second.node, first.node,
+43 -35
View File
@@ -585,41 +585,49 @@ fn find_wayland_socket(env: &EnvProbe, runtime: &str, uid: u32) -> Option<String
pub fn apply_session_env(active: &ActiveSession) {
let _env_guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let e = &active.env;
std::env::set_var("XDG_RUNTIME_DIR", &e.xdg_runtime_dir);
std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &e.dbus_session_bus_address);
if let Some(w) = &e.wayland_display {
std::env::set_var("WAYLAND_DISPLAY", w);
}
if let Some(d) = &e.xdg_current_desktop {
std::env::set_var("XDG_CURRENT_DESKTOP", d);
}
// Hyprland: export the discovered instance signature so `hyprctl` reaches the live compositor
// (fixes G4 for the systemd `--user` host, which never inherited it). Only set when detection
// found a Hyprland session; a stale value from a previous connect is cleared otherwise so a
// Hyprland→sway switch can't leave `hyprctl` pointed at a dead instance.
match &e.hyprland_signature {
Some(sig) => std::env::set_var("HYPRLAND_INSTANCE_SIGNATURE", sig),
None => std::env::remove_var("HYPRLAND_INSTANCE_SIGNATURE"),
}
// sway: same treatment, and for the same reason — `swaymsg` (output enumeration, the capture
// chooser) is unreachable without it, so a systemd `--user` host that never inherited the login
// environment had no sway backend at all. Cleared when nothing sway-shaped is live, so a
// sway→Hyprland switch can't leave `swaymsg` aimed at a dead socket. `wlroots::is_available()`
// keys off this variable, so setting it here is also what makes the backend visible at all.
match &e.sway_socket {
Some(sock) => std::env::set_var("SWAYSOCK", sock),
None => std::env::remove_var("SWAYSOCK"),
}
// NOTHING live ⇒ every session-scoped var still in the env is a leftover from a previous
// connect's retarget, and the availability probes read them: after a gnome-shell crash
// (observed 2026-07-10: SIGSEGV → GDM greeter) a stale `XDG_CURRENT_DESKTOP=GNOME` kept
// `mutter::is_available()` true, so a client's explicit backend request routed into the dead
// session — 45 s create timeouts and a libei error loop instead of the crisp "no live
// graphical session" handshake error. Clear them so `available()` reports the truth and the
// client fails fast (and, when configured, `try_recover_session` can bring the desktop back).
if active.kind == ActiveKind::None {
std::env::remove_var("XDG_CURRENT_DESKTOP");
std::env::remove_var("WAYLAND_DISPLAY");
// SAFETY: `_env_guard` holds [`ENV_LOCK`] — the crate-wide discipline (see its doc in lib.rs)
// that serializes every process-env writer on the session-setup path; steady-state streaming
// threads read cached config, not the environment (security-review 2026-06-28 #7).
unsafe {
std::env::set_var("XDG_RUNTIME_DIR", &e.xdg_runtime_dir);
std::env::set_var("DBUS_SESSION_BUS_ADDRESS", &e.dbus_session_bus_address);
if let Some(w) = &e.wayland_display {
std::env::set_var("WAYLAND_DISPLAY", w);
}
if let Some(d) = &e.xdg_current_desktop {
std::env::set_var("XDG_CURRENT_DESKTOP", d);
}
// Hyprland: export the discovered instance signature so `hyprctl` reaches the live
// compositor (fixes G4 for the systemd `--user` host, which never inherited it). Only set
// when detection found a Hyprland session; a stale value from a previous connect is
// cleared otherwise so a Hyprland→sway switch can't leave `hyprctl` pointed at a dead
// instance.
match &e.hyprland_signature {
Some(sig) => std::env::set_var("HYPRLAND_INSTANCE_SIGNATURE", sig),
None => std::env::remove_var("HYPRLAND_INSTANCE_SIGNATURE"),
}
// sway: same treatment, and for the same reason — `swaymsg` (output enumeration, the
// capture chooser) is unreachable without it, so a systemd `--user` host that never
// inherited the login environment had no sway backend at all. Cleared when nothing
// sway-shaped is live, so a sway→Hyprland switch can't leave `swaymsg` aimed at a dead
// socket. `wlroots::is_available()` keys off this variable, so setting it here is also
// what makes the backend visible at all.
match &e.sway_socket {
Some(sock) => std::env::set_var("SWAYSOCK", sock),
None => std::env::remove_var("SWAYSOCK"),
}
// NOTHING live ⇒ every session-scoped var still in the env is a leftover from a previous
// connect's retarget, and the availability probes read them: after a gnome-shell crash
// (observed 2026-07-10: SIGSEGV → GDM greeter) a stale `XDG_CURRENT_DESKTOP=GNOME` kept
// `mutter::is_available()` true, so a client's explicit backend request routed into the
// dead session — 45 s create timeouts and a libei error loop instead of the crisp "no
// live graphical session" handshake error. Clear them so `available()` reports the truth
// and the client fails fast (and, when configured, `try_recover_session` can bring the
// desktop back).
if active.kind == ActiveKind::None {
std::env::remove_var("XDG_CURRENT_DESKTOP");
std::env::remove_var("WAYLAND_DISPLAY");
}
}
// Topology (Stage 2): the per-compositor backends (KWin/Mutter) now read
// [`effective_topology`] directly at create time — the console policy, else the legacy
@@ -112,8 +112,8 @@ struct Monitor {
/// This monitor's desktop-space origin from the group layout (`(0,0)` until a multi-slot
/// arrangement places it) — reported via [`ManagedInfo`].
position: (i32, i32),
/// Generation stamp; a [`MonitorLease`] only releases if its gen still matches (stale-lease no-op).
gen: u64,
/// Generation stamp; a [`MonitorLease`] only releases if its generation still matches (stale-lease no-op).
generation: u64,
}
impl Monitor {
@@ -371,10 +371,10 @@ struct MgrInner {
}
impl MgrInner {
/// Live target ids in acquire (gen) order — the CCD isolate keep-set + the layout member order.
/// Live target ids in acquire (generation) order — the CCD isolate keep-set + the layout member order.
fn target_ids(&self) -> Vec<u32> {
let mut mons: Vec<&Monitor> = self.slots.values().map(SlotState::mon).collect();
mons.sort_by_key(|m| m.gen);
mons.sort_by_key(|m| m.generation);
mons.iter().map(|m| m.target_id).collect()
}
}
@@ -429,7 +429,7 @@ pub struct VirtualDisplayManager {
/// fallback. One attempt per process, in case a future OS build honors the refresh.
update_modes_futile: AtomicBool,
/// Monotonic lease-generation counter (was the `MON_GEN` global).
gen: AtomicU64,
generation: AtomicU64,
state: Mutex<MgrInner>,
/// Serializes IDD-push session SETUP (preempt + monitor create) — MANAGER-WIDE even with slots:
/// monitor create/teardown stays serialized (the 400 ms async-departure settle and the IddCx
@@ -477,7 +477,7 @@ pub(crate) fn init(driver: Box<dyn VdisplayDriver>) -> &'static VirtualDisplayMa
watchdog_s: AtomicU32::new(3),
driver_proto: AtomicU32::new(0),
update_modes_futile: AtomicBool::new(false),
gen: AtomicU64::new(1),
generation: AtomicU64::new(1),
state: Mutex::new(MgrInner::default()),
setup_lock: Mutex::new(()),
idd_session_stops: Mutex::new(std::collections::HashMap::new()),
@@ -704,7 +704,7 @@ impl VirtualDisplayManager {
// IDD-push: a new connection while THIS SLOT's monitor is kept (LINGERING or PINNED) is a
// single-client RECONNECT (the prior session fully released). A REUSED IddCx swap-chain is
// DEAD, so reusing it hands a black screen — PREEMPT: tear the kept monitor down and create a
// fresh one. The old session's lease is gen-stamped, so its later drop is a no-op. A SIBLING
// fresh one. The old session's lease is generation-stamped, so its later drop is a no-op. A SIBLING
// slot's kept monitor is never touched — that's another client's display.
//
// ONLY the kept states, NOT Active: an Active monitor still has a lease held — that's the
@@ -750,7 +750,7 @@ impl VirtualDisplayManager {
// hand the rebuild the dead monitor's target (stale wudf_pid) and starve it to the rebuild
// budget. Preempt instead: best-effort teardown (REMOVE fails harmlessly on a dead/retired
// device) and fall through to a fresh create on the auto-restarted device. Held leases are
// gen-stamped, so their eventual release is a no-op. ONE WUDFHost hosts every slot's
// generation-stamped, so their eventual release is a no-op. ONE WUDFHost hosts every slot's
// publisher, so its death is ALL-slot shared fate — but each sibling's session fails through
// its own capturer watch and rebuilds through this same path; no cross-slot teardown here.
if matches!(inner.slots.get(&slot), Some(SlotState::Active { mon, .. }) if !wudf_alive(mon.wudf_pid))
@@ -811,7 +811,7 @@ impl VirtualDisplayManager {
match unsafe { self.resize_in_place(dev_raw(&dev), mon, mode) } {
Ok(()) => {
// Same join semantics as the re-arrival: +1 ref for the new
// (build-then-drop overlap) lease; `gen` untouched, so the old
// (build-then-drop overlap) lease; `generation` untouched, so the old
// session's lease stays valid.
*refs += 1;
let refs = *refs;
@@ -862,7 +862,7 @@ impl VirtualDisplayManager {
// acked the switch; Fix 2's corrective ack tells the client the resolution
// did not change). Store the RECOVERED monitor, not the one handed in:
// that one's driver monitor was REMOVEd before the failed ADD, so its
// `key`/`target_id`/`gdi_name` are all dead. `gen`/`refs` are preserved,
// `key`/`target_id`/`gdi_name` are all dead. `generation`/`refs` are preserved,
// so leases stay valid either way.
inner.slots.insert(
slot,
@@ -879,7 +879,7 @@ impl VirtualDisplayManager {
return Err(err).context("mid-stream resize re-arrival (slot left empty)");
}
};
// `re_add` preserved `gen`, so both the old session's lease and this new one match on
// `re_add` preserved `generation`, so both the old session's lease and this new one match on
// release. +1 ref for the new (build-then-drop overlap) lease.
let out = self.output_for(slot, &new_mon, quit);
inner.slots.insert(
@@ -969,7 +969,7 @@ impl VirtualDisplayManager {
Ok(out)
}
/// Build the [`VirtualOutput`] (preferred mode + capture target + a fresh gen-stamped lease) for
/// Build the [`VirtualOutput`] (preferred mode + capture target + a fresh generation-stamped lease) for
/// `mon` in `slot`. `quit` is the session's deliberate-quit flag, read by the lease `Drop` (see
/// [`Self::release`]).
fn output_for(
@@ -985,7 +985,7 @@ impl VirtualDisplayManager {
keepalive: Box::new(MonitorLease {
mgr: self,
slot,
gen: mon.gen,
generation: mon.generation,
quit,
}),
// The Windows manager owns the monitor lifecycle (refcount/linger/pin), so the registry
@@ -1215,18 +1215,18 @@ impl VirtualDisplayManager {
return;
}
let layout_policy = crate::policy::prefs().get().effective().layout;
// Members in acquire (gen) order — the auto-row order; identity slot 0 = anonymous (no
// manual pin can address it, so it always auto-rows). `(slot, gen, target_id, width)`
// Members in acquire (generation) order — the auto-row order; identity slot 0 = anonymous (no
// manual pin can address it, so it always auto-rows). `(slot, generation, target_id, width)`
// copied out so the arrangement below can write back through `get_mut`.
let mut ordered: Vec<(u32, u64, u32, i32)> = inner
.slots
.iter()
.map(|(slot, s)| {
let m = s.mon();
(*slot, m.gen, m.target_id, m.mode.width as i32)
(*slot, m.generation, m.target_id, m.mode.width as i32)
})
.collect();
ordered.sort_by_key(|&(_, gen, _, _)| gen);
ordered.sort_by_key(|&(_, generation, _, _)| generation);
let members: Vec<Member> = ordered
.iter()
.map(|&(slot, _, _, width)| Member {
@@ -1569,7 +1569,7 @@ impl VirtualDisplayManager {
requested_mode,
resolved_monitor_id: added.resolved_monitor_id,
position: (0, 0),
gen: self.gen.fetch_add(1, Ordering::Relaxed),
generation: self.generation.fetch_add(1, Ordering::Relaxed),
hw_cursor,
cursor_excluded: added.cursor_excluded,
})
@@ -1693,7 +1693,7 @@ impl VirtualDisplayManager {
/// / ContainerId) so the OS keeps the monitor's identity + saved per-monitor DPI. The visible cost
/// is one monitor hotplug per switch (the design's accepted "re-arrival for everything").
///
/// Refcount/lease continuity: the rebuilt `Monitor` PRESERVES the old `gen`, so the outstanding
/// Refcount/lease continuity: the rebuilt `Monitor` PRESERVES the old `generation`, so the outstanding
/// session lease(s) still match on release — the linger/refcount machine is untouched. The group
/// restore snapshot (`group.ccd_saved` / DDC / PnP) is likewise PRESERVED (a mid-session swap, not
/// a first-member create): [`reisolate_after_swap`](Self::reisolate_after_swap) re-isolates the new
@@ -1837,8 +1837,8 @@ impl VirtualDisplayManager {
added.target_id
),
}
// 5. Rebuild the Monitor from the ADD reply, PRESERVING `gen` (lease/refcount continuity) and
// the group-layout `position`. A fresh `gen` would strand the old session's lease release.
// 5. Rebuild the Monitor from the ADD reply, PRESERVING `generation` (lease/refcount continuity) and
// the group-layout `position`. A fresh `generation` would strand the old session's lease release.
let mon = Box::new(Monitor {
key: added.key,
target_id: added.target_id,
@@ -1850,7 +1850,7 @@ impl VirtualDisplayManager {
requested_mode,
resolved_monitor_id: added.resolved_monitor_id,
position: old.position,
gen: old.gen,
generation: old.generation,
hw_cursor: old.hw_cursor,
// Fresh from THIS reply, not `old`: the driver's per-target declare registry is the
// ground truth (this session may itself have declared since the original ADD).
@@ -2048,10 +2048,10 @@ impl VirtualDisplayManager {
/// deliberate quit still pins — only `/display/release` frees a pinned monitor. A STALE lease
/// (its monitor was preempted + recreated under it) is a no-op, so it can't tear down the
/// CURRENT monitor.
fn release(&self, slot: u32, gen: u64, quit_now: bool) {
fn release(&self, slot: u32, generation: u64, quit_now: bool) {
let mut inner = self.state.lock().unwrap();
let stale = match inner.slots.get(&slot) {
Some(s) => s.mon().gen != gen,
Some(s) => s.mon().generation != generation,
None => true,
};
if stale {
@@ -2160,7 +2160,7 @@ impl VirtualDisplayManager {
// HERE instead. This runs at most ONCE per session (we hold `setup_lock`), so —
// unlike preempting inside `acquire` — it does not reintroduce the per-retry churn.
// The next `acquire` then sees the slot empty and creates a fresh monitor; the stale
// session's gen-stamped lease release is a no-op.
// session's generation-stamped lease release is a no-op.
if let Some(dev) = self.device_handle() {
let mut inner = self.state.lock().unwrap();
let taken = match inner.slots.get(&slot) {
@@ -2220,38 +2220,40 @@ impl VirtualDisplayManager {
TIMER.call_once(|| {
thread::Builder::new()
.name("vdisplay-linger".into())
.spawn(move || loop {
thread::sleep(Duration::from_millis(500));
let Some(dev) = self.device_handle() else {
continue;
};
let mut g = self.state.lock().unwrap();
let now = Instant::now();
let expired: Vec<u32> = g
.slots
.iter()
.filter_map(|(slot, s)| {
matches!(s, SlotState::Lingering { until, .. } if now >= *until)
.then_some(*slot)
})
.collect();
for slot in expired {
if let Some(SlotState::Lingering { mon, .. }) = g.slots.remove(&slot) {
// Teardown UNDER the state lock. Dropping the lock first (the old shape)
// let a concurrent `acquire` see the slot empty and run its ADD + CCD
// isolate while this monitor's CCD-restore / REMOVE were still in flight
// — the late restore then de-isolated (or the REMOVE churn-rejected) the
// fresh session at the linger-expiry boundary. Holding the lock makes
// the racing acquire WAIT the few teardown seconds instead of failing
// its session. Lock order stays state → device (teardown's invalidate
// path), same as every other holder; the pinger takes only the device
// lock — no inversion.
// SAFETY: `teardown_removed` requires a valid control handle; the `dev`
// Arc from `self.device_handle()` is held across this call, so the
// handle stays open (a concurrent retire drops only the manager's
// reference; see `DeviceSlot`). `mon` was moved out of the map under
// the lock, so it is exclusively owned here.
unsafe { self.teardown_removed(dev_raw(&dev), &mut g, mon) };
.spawn(move || {
loop {
thread::sleep(Duration::from_millis(500));
let Some(dev) = self.device_handle() else {
continue;
};
let mut g = self.state.lock().unwrap();
let now = Instant::now();
let expired: Vec<u32> = g
.slots
.iter()
.filter_map(|(slot, s)| {
matches!(s, SlotState::Lingering { until, .. } if now >= *until)
.then_some(*slot)
})
.collect();
for slot in expired {
if let Some(SlotState::Lingering { mon, .. }) = g.slots.remove(&slot) {
// Teardown UNDER the state lock. Dropping the lock first (the old shape)
// let a concurrent `acquire` see the slot empty and run its ADD + CCD
// isolate while this monitor's CCD-restore / REMOVE were still in flight
// — the late restore then de-isolated (or the REMOVE churn-rejected) the
// fresh session at the linger-expiry boundary. Holding the lock makes
// the racing acquire WAIT the few teardown seconds instead of failing
// its session. Lock order stays state → device (teardown's invalidate
// path), same as every other holder; the pinger takes only the device
// lock — no inversion.
// SAFETY: `teardown_removed` requires a valid control handle; the `dev`
// Arc from `self.device_handle()` is held across this call, so the
// handle stays open (a concurrent retire drops only the manager's
// reference; see `DeviceSlot`). `mon` was moved out of the map under
// the lock, so it is exclusively owned here.
unsafe { self.teardown_removed(dev_raw(&dev), &mut g, mon) };
}
}
}
})
@@ -2265,7 +2267,7 @@ impl VirtualDisplayManager {
struct MonitorLease {
mgr: &'static VirtualDisplayManager,
slot: u32,
gen: u64,
generation: u64,
/// The session's deliberate-quit flag (the client closed with the QUIT application code — a user
/// "stop", not a network drop). Read at drop time: a quit release tears the monitor down NOW
/// instead of lingering, mirroring the Linux registry's `Linger::Immediate`. `None` = no signal
@@ -2276,7 +2278,7 @@ struct MonitorLease {
impl Drop for MonitorLease {
fn drop(&mut self) {
let quit_now = self.quit.as_ref().is_some_and(|q| q.load(Ordering::SeqCst));
self.mgr.release(self.slot, self.gen, quit_now);
self.mgr.release(self.slot, self.generation, quit_now);
}
}
@@ -2344,7 +2346,7 @@ pub(crate) struct ManagedInfo {
/// Live sessions holding the monitor.
pub sessions: u32,
/// The monitor's generation stamp — a stable-enough id for the `/display/release` slot arg.
pub gen: u64,
pub generation: u64,
/// The slot key: the client's stable identity slot (`1..=15`), or `0` = anonymous/auto.
pub slot_id: u32,
/// Desktop-space origin from the group layout (`(0,0)` for a single display).
@@ -2352,7 +2354,7 @@ pub(crate) struct ManagedInfo {
}
impl VirtualDisplayManager {
/// Snapshot the managed slots for the mgmt `/display/state` endpoint, in acquire (gen) order.
/// Snapshot the managed slots for the mgmt `/display/state` endpoint, in acquire (generation) order.
/// Empty when no slot lives.
pub(crate) fn snapshot(&self) -> Vec<ManagedInfo> {
let inner = self.state.lock().unwrap();
@@ -2376,20 +2378,20 @@ impl VirtualDisplayManager {
state,
expires_in_ms,
sessions,
gen: mon.gen,
generation: mon.generation,
slot_id: *slot,
position: mon.position,
}
})
.collect();
out.sort_by_key(|i| i.gen);
out.sort_by_key(|i| i.generation);
out
}
/// Force-tear-down kept (LINGERING **or** PINNED) monitors now (the `/display/release` endpoint) —
/// so a physical-screen user gets their screen back without waiting out the linger, and it is the §8
/// escape hatch that frees a `keep_alive=forever` (Pinned) monitor. `slot` selects one kept monitor
/// by its [`ManagedInfo::gen`] stamp; `None` releases every kept one. Active monitors are refused
/// by its [`ManagedInfo::generation`] stamp; `None` releases every kept one. Active monitors are refused
/// (stopping a live session is session management, not display management). Returns the number
/// released.
pub(crate) fn force_release(&self, slot: Option<u64>) -> usize {
@@ -2402,7 +2404,7 @@ impl VirtualDisplayManager {
.iter()
.filter_map(|(k, s)| match s {
SlotState::Lingering { mon, .. } | SlotState::Pinned { mon }
if slot.is_none_or(|g| g == mon.gen) =>
if slot.is_none_or(|g| g == mon.generation) =>
{
Some(*k)
}
@@ -2434,7 +2436,7 @@ pub(crate) fn snapshot() -> Vec<ManagedInfo> {
.unwrap_or_default()
}
/// Force-release kept monitors now (`slot` = a [`ManagedInfo::gen`] stamp, `None` = all kept); `0`
/// Force-release kept monitors now (`slot` = a [`ManagedInfo::generation`] stamp, `None` = all kept); `0`
/// if nothing was kept (or the manager is uninitialised).
pub(crate) fn force_release(slot: Option<u64>) -> usize {
VDM.get().map(|m| m.force_release(slot)).unwrap_or(0)
+1 -1
View File
@@ -268,7 +268,7 @@ fn flagged(flags: &[bool]) -> Vec<usize> {
flags
.iter()
.enumerate()
.filter(|(_, &d)| d)
.filter(|&(_, &d)| d)
.map(|(i, _)| i)
.collect()
}
+15 -5
View File
@@ -830,7 +830,9 @@ fn h264_parity_run_against(
// The decoder reads this at session creation (first decode call): pool
// images grow TRANSFER_SRC so vkCmdCopyImageToBuffer is legal.
std::env::set_var("PF_VKD_TEST_READBACK", "1");
// SAFETY: `_gpu` holds the binary-wide GPU lock (`common::gpu_lock`); the parity legs are
// this variable's only writers and readers, and they run one at a time under that lock.
unsafe { std::env::set_var("PF_VKD_TEST_READBACK", "1") };
let goldens = golden_hashes(goldens);
assert_eq!(
@@ -940,7 +942,9 @@ fn h265_parity_run(
// As the H.264 leg: one codec at a time, `set_var` under the lock.
let _gpu = common::gpu_lock();
std::env::set_var("PF_VKD_TEST_READBACK", "1");
// SAFETY: `_gpu` holds the binary-wide GPU lock (`common::gpu_lock`); the parity legs are
// this variable's only writers and readers, and they run one at a time under that lock.
unsafe { std::env::set_var("PF_VKD_TEST_READBACK", "1") };
let goldens = golden_hashes(goldens_file);
assert_eq!(
@@ -1124,7 +1128,9 @@ fn av1_parity_run_against(
// happens only under this lock (see `common::gpu_lock`).
let _gpu = common::gpu_lock();
std::env::set_var("PF_VKD_TEST_READBACK", "1");
// SAFETY: `_gpu` holds the binary-wide GPU lock (`common::gpu_lock`); the parity legs are
// this variable's only writers and readers, and they run one at a time under that lock.
unsafe { std::env::set_var("PF_VKD_TEST_READBACK", "1") };
let goldens = golden_hashes(goldens_file);
// Non-vacuity, before any hardware is touched: the right number of entries, all
@@ -1265,7 +1271,9 @@ fn low_delay_host_av1_every_frame_hashes_bit_identical_to_libavcodec() {
#[ignore = "needs a Vulkan Video AV1 decode device (fleet boxes; see module docs)"]
fn av1_frame0_pixels_say_which_plane_and_how_badly() {
let _gpu = common::gpu_lock();
std::env::set_var("PF_VKD_TEST_READBACK", "1");
// SAFETY: `_gpu` holds the binary-wide GPU lock (`common::gpu_lock`); the parity legs are
// this variable's only writers and readers, and they run one at a time under that lock.
unsafe { std::env::set_var("PF_VKD_TEST_READBACK", "1") };
let aus = common::split_av1_aus(common::TEST_25FPS_AV1);
assert_eq!(
@@ -1338,7 +1346,9 @@ const MAX_LOOP_FILTER_LEVEL: u8 = 63;
#[ignore = "needs a Vulkan Video AV1 decode device (fleet boxes; see module docs)"]
fn av1_frame0_probes_whether_the_driver_reads_the_chroma_deblocking_levels() {
let _gpu = common::gpu_lock();
std::env::set_var("PF_VKD_TEST_READBACK", "1");
// SAFETY: `_gpu` holds the binary-wide GPU lock (`common::gpu_lock`); the parity legs are
// this variable's only writers and readers, and they run one at a time under that lock.
unsafe { std::env::set_var("PF_VKD_TEST_READBACK", "1") };
let aus = common::split_av1_aus(common::TEST_25FPS_AV1);
assert_eq!(aus.len(), FRAME_COUNT);
+1 -1
View File
@@ -7,7 +7,7 @@
[package]
name = "pf-win-display"
version.workspace = true
edition = "2021"
edition.workspace = true
rust-version.workspace = true
license = "MIT OR Apache-2.0"
description = "punktfunk host Windows display-topology helpers: CCD/GDI mode-set + path activation, HDR advanced colour, PnP monitor devnodes, and the display-change event watch."
+14 -5
View File
@@ -1317,11 +1317,14 @@ pub mod isolate_journal {
let dir = std::env::temp_dir().join(format!("pf-isolate-journal-{name}"));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("scratch dir");
std::env::set_var("PUNKTFUNK_CONFIG_DIR", &dir);
// SAFETY: `_g` holds this module's ENV mutex, which serializes every test that
// writes or reads `PUNKTFUNK_CONFIG_DIR` in this binary.
unsafe { std::env::set_var("PUNKTFUNK_CONFIG_DIR", &dir) };
clear(); // reset the LAST cache + any leftover marker from a previous run
f(&dir);
clear();
std::env::remove_var("PUNKTFUNK_CONFIG_DIR");
// SAFETY: still under `_g` — the same serialization as the set above.
unsafe { std::env::remove_var("PUNKTFUNK_CONFIG_DIR") };
let _ = std::fs::remove_dir_all(&dir);
}
@@ -1529,10 +1532,14 @@ pub fn isolate_displays_ccd(keep_target_ids: &[u32]) -> Option<SavedConfig> {
// and confirm no non-keep display survived. Only then is the virtual set truly the sole desktop.
let survivors = count_other_active(keep_target_ids).unwrap_or(0);
if survivors == 0 {
tracing::info!("display isolate (CCD): target set {keep_target_ids:?} is the SOLE active desktop (attempt {attempt}/4, deactivated {others}, rc={rc:#x})");
tracing::info!(
"display isolate (CCD): target set {keep_target_ids:?} is the SOLE active desktop (attempt {attempt}/4, deactivated {others}, rc={rc:#x})"
);
return Some(saved);
}
tracing::warn!("display isolate (CCD): {survivors} display(s) STILL active after attempt {attempt}/4 (deactivated {others}, rc={rc:#x}) — re-querying + retrying");
tracing::warn!(
"display isolate (CCD): {survivors} display(s) STILL active after attempt {attempt}/4 (deactivated {others}, rc={rc:#x}) — re-querying + retrying"
);
std::thread::sleep(std::time::Duration::from_millis(250));
}
// Name the survivors instead of assuming their kind — the field logs showed this path fire
@@ -1932,7 +1939,9 @@ pub fn set_virtual_primary_ccd(keep_target_id: u32) -> Option<SavedConfig> {
)
});
if rc == 0 {
tracing::info!("display primary (CCD): virtual target {keep_target_id} set PRIMARY at (0,0); {others} other display(s) kept ACTIVE + packed to its right");
tracing::info!(
"display primary (CCD): virtual target {keep_target_id} set PRIMARY at (0,0); {others} other display(s) kept ACTIVE + packed to its right"
);
} else {
tracing::warn!(
"display primary (CCD): SetDisplayConfig failed rc={rc:#x}{} (virtual {keep_target_id} primary, physicals kept)",
+1 -1
View File
@@ -6,7 +6,7 @@
[package]
name = "pf-zerocopy"
version.workspace = true
edition = "2021"
edition.workspace = true
# Inherit the workspace MSRV: clippy keys MSRV-gated lints off it (without this, e.g.
# `manual_is_multiple_of` fires on code the host crate compiles clean).
rust-version.workspace = true
+2 -2
View File
@@ -34,7 +34,7 @@ pub(crate) const GL_LINK_STATUS: u32 = 0x8B82;
// libglvnd's libGL dispatches these to the NVIDIA driver based on the current EGL/GL context.
#[link(name = "GL")]
extern "C" {
unsafe extern "C" {
pub(crate) fn glGenTextures(n: c_int, textures: *mut u32);
pub(crate) fn glBindTexture(target: u32, texture: u32);
pub(crate) fn glTexParameteri(target: u32, pname: u32, param: c_int);
@@ -97,7 +97,7 @@ extern "C" {
}
#[link(name = "gbm")]
extern "C" {
unsafe extern "C" {
pub(crate) fn gbm_create_device(fd: c_int) -> *mut c_void;
pub(crate) fn gbm_device_destroy(device: *mut c_void);
}
+85 -85
View File
@@ -253,7 +253,7 @@ fn new_handle(session: Session) -> *mut PunktfunkSession {
}
/// Current ABI version. Mismatch with [`crate::ABI_VERSION`] means incompatible core.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "C" fn punktfunk_abi_version() -> u32 {
crate::ABI_VERSION
}
@@ -271,7 +271,7 @@ pub extern "C" fn punktfunk_abi_version() -> u32 {
/// # Safety
/// `macs` must point to at least `mac_count * 6` readable bytes. `last_known_ip`, if non-NULL,
/// must be a NUL-terminated string.
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_wake_on_lan(
macs: *const u8,
mac_count: usize,
@@ -321,7 +321,7 @@ pub unsafe extern "C" fn punktfunk_wake_on_lan(
///
/// # Safety
/// `cfg`, `local`, `peer` must be valid pointers; the strings must be NUL-terminated.
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_session_new(
cfg: *const PunktfunkConfig,
local: *const c_char,
@@ -366,7 +366,7 @@ pub unsafe extern "C" fn punktfunk_session_new(
///
/// # Safety
/// All four pointers must be valid; the two out-params receive owned handles.
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_test_loopback_pair(
host_cfg: *const PunktfunkConfig,
client_cfg: *const PunktfunkConfig,
@@ -413,7 +413,7 @@ pub unsafe extern "C" fn punktfunk_test_loopback_pair(
///
/// # Safety
/// `s` must be a handle from `punktfunk_session_new`/`punktfunk_test_loopback_pair`, freed once.
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_session_free(s: *mut PunktfunkSession) {
guard_void(|| {
if !s.is_null() {
@@ -428,7 +428,7 @@ pub unsafe extern "C" fn punktfunk_session_free(s: *mut PunktfunkSession) {
///
/// # Safety
/// `s` is a valid host handle; `data` points to `len` readable bytes (or `len == 0`).
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_host_submit_frame(
s: *mut PunktfunkSession,
data: *const u8,
@@ -466,7 +466,7 @@ pub unsafe extern "C" fn punktfunk_host_submit_frame(
///
/// # Safety
/// `s` is a valid client handle; `out` points to a writable `PunktfunkFrame`.
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_client_poll_frame(
s: *mut PunktfunkSession,
out: *mut PunktfunkFrame,
@@ -510,7 +510,7 @@ pub unsafe extern "C" fn punktfunk_client_poll_frame(
///
/// # Safety
/// `s` is a valid client handle; `ev` points to a readable `InputEvent`-sized allocation.
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_send_input(
s: *mut PunktfunkSession,
ev: *const InputEvent,
@@ -566,7 +566,7 @@ unsafe fn read_input_event<'a>(ev: *const InputEvent) -> Result<&'a InputEvent,
///
/// # Safety
/// `s` is a valid host handle; `user` is passed back verbatim to `cb`.
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_set_input_callback(
s: *mut PunktfunkSession,
// Written as an explicit `Option<fn>` (not the `PunktfunkInputCb` alias) so cbindgen
@@ -592,7 +592,7 @@ pub unsafe extern "C" fn punktfunk_set_input_callback(
///
/// # Safety
/// `s` is a valid host handle.
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_host_poll_input(s: *mut PunktfunkSession) -> i32 {
let r = std::panic::catch_unwind(AssertUnwindSafe(|| {
let mut count = 0i32;
@@ -633,7 +633,7 @@ pub unsafe extern "C" fn punktfunk_host_poll_input(s: *mut PunktfunkSession) ->
///
/// # Safety
/// `s` is a valid handle; `out` points to a writable `PunktfunkStats`.
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_get_stats(
s: *mut PunktfunkSession,
out: *mut PunktfunkStats,
@@ -1427,7 +1427,7 @@ const _: () = {
/// `pin_sha256`/`observed_sha256_out` are each NULL or valid for 32 bytes;
/// `client_cert_pem`/`client_key_pem` are each NULL or NUL-terminated UTF-8.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connect(
host: *const std::os::raw::c_char,
port: u16,
@@ -1468,7 +1468,7 @@ pub unsafe extern "C" fn punktfunk_connect(
/// # Safety
/// Same as [`punktfunk_connect`].
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connect_ex(
host: *const std::os::raw::c_char,
port: u16,
@@ -1512,7 +1512,7 @@ pub unsafe extern "C" fn punktfunk_connect_ex(
/// # Safety
/// Same as [`punktfunk_connect`].
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connect_ex2(
host: *const std::os::raw::c_char,
port: u16,
@@ -1557,7 +1557,7 @@ pub unsafe extern "C" fn punktfunk_connect_ex2(
/// # Safety
/// Same as [`punktfunk_connect`].
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connect_ex3(
host: *const std::os::raw::c_char,
port: u16,
@@ -1605,7 +1605,7 @@ pub unsafe extern "C" fn punktfunk_connect_ex3(
/// # Safety
/// Same as [`punktfunk_connect`]; `launch_id`, when non-NULL, must be a NUL-terminated C string.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connect_ex4(
host: *const std::os::raw::c_char,
port: u16,
@@ -1658,7 +1658,7 @@ pub unsafe extern "C" fn punktfunk_connect_ex4(
/// # Safety
/// Same as [`punktfunk_connect`]; `launch_id`, when non-NULL, must be a NUL-terminated C string.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
#[allow(clippy::too_many_arguments)]
pub unsafe extern "C" fn punktfunk_connect_ex5(
host: *const std::os::raw::c_char,
@@ -1711,7 +1711,7 @@ pub unsafe extern "C" fn punktfunk_connect_ex5(
/// # Safety
/// Same as [`punktfunk_connect`].
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
#[allow(clippy::too_many_arguments)]
pub unsafe extern "C" fn punktfunk_connect_ex6(
host: *const std::os::raw::c_char,
@@ -1767,7 +1767,7 @@ pub unsafe extern "C" fn punktfunk_connect_ex6(
/// # Safety
/// Same as [`punktfunk_connect`].
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
#[allow(clippy::too_many_arguments)]
pub unsafe extern "C" fn punktfunk_connect_ex7(
host: *const std::os::raw::c_char,
@@ -1828,7 +1828,7 @@ pub unsafe extern "C" fn punktfunk_connect_ex7(
/// # Safety
/// Same as [`punktfunk_connect`]; `status_out`, when non-null, must point to a writable `i32`.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
#[allow(clippy::too_many_arguments)]
pub unsafe extern "C" fn punktfunk_connect_ex8(
host: *const std::os::raw::c_char,
@@ -1889,7 +1889,7 @@ pub unsafe extern "C" fn punktfunk_connect_ex8(
/// # Safety
/// Same as [`punktfunk_connect_ex8`].
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
#[allow(clippy::too_many_arguments)]
pub unsafe extern "C" fn punktfunk_connect_ex9(
host: *const std::os::raw::c_char,
@@ -2122,7 +2122,7 @@ unsafe fn connect_ex_impl(
/// # Safety
/// `cert_pem_out` is writable for `cert_cap` bytes; `key_pem_out` for `key_cap`.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_generate_identity(
cert_pem_out: *mut std::os::raw::c_char,
cert_cap: usize,
@@ -2165,7 +2165,7 @@ pub unsafe extern "C" fn punktfunk_generate_identity(
/// # Safety
/// `host` must be a NUL-terminated UTF-8 string.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_probe(
host: *const std::os::raw::c_char,
port: u16,
@@ -2200,7 +2200,7 @@ pub unsafe extern "C" fn punktfunk_probe(
/// `host`/`client_cert_pem`/`client_key_pem`/`pin`/`name` are NUL-terminated UTF-8;
/// `host_sha256_out` is writable for 32 bytes.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_pair(
host: *const std::os::raw::c_char,
port: u16,
@@ -2264,7 +2264,7 @@ pub unsafe extern "C" fn punktfunk_pair(
/// `c` is a valid connection handle; `out` is writable. At most one thread pulls video —
/// it may run concurrently with one audio-pulling and one rumble-pulling thread.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_next_au(
c: *mut PunktfunkConnection,
out: *mut PunktfunkFrame,
@@ -2329,7 +2329,7 @@ pub struct PunktfunkAudioPacket {
/// `c` is a valid connection handle; `out` is writable. At most one thread pulls audio —
/// it may run concurrently with the video/rumble pullers.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_next_audio(
c: *mut PunktfunkConnection,
out: *mut PunktfunkAudioPacket,
@@ -2380,7 +2380,7 @@ pub unsafe extern "C" fn punktfunk_connection_next_audio(
/// # Safety
/// `c` is a valid connection handle; `out` is NULL or writable for one `u8`.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_audio_channels(
c: *mut PunktfunkConnection,
out: *mut u8,
@@ -2420,7 +2420,7 @@ pub unsafe extern "C" fn punktfunk_connection_audio_channels(
/// # Safety
/// `c` is a valid connection handle; `out` is NULL or writable for one `u8`.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_end_reason(
c: *mut PunktfunkConnection,
out: *mut u8,
@@ -2478,7 +2478,7 @@ pub struct PunktfunkAudioPcm {
/// # Safety
/// `c` is a valid connection handle; `out` is writable. At most one thread pulls audio.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_next_audio_pcm(
c: *mut PunktfunkConnection,
out: *mut PunktfunkAudioPcm,
@@ -2544,7 +2544,7 @@ pub unsafe extern "C" fn punktfunk_connection_next_audio_pcm(
/// `c` is a valid connection handle; the `out_*` pointers are writable (NULLs are skipped);
/// `buf` is writable for `buf_len` bytes.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_next_pad_audio(
c: *mut PunktfunkConnection,
out_pad: *mut u8,
@@ -2618,7 +2618,7 @@ pub unsafe extern "C" fn punktfunk_connection_next_pad_audio(
/// # Safety
/// `c` is a valid connection handle. Callable from any thread.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_set_pad_audio_caps(
c: *mut PunktfunkConnection,
pad: u8,
@@ -2649,7 +2649,7 @@ pub unsafe extern "C" fn punktfunk_connection_set_pad_audio_caps(
/// `c` is a valid connection handle; out pointers are writable (NULLs are skipped). At
/// most one thread pulls rumble — it may run concurrently with the video/audio pullers.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_next_rumble(
c: *mut PunktfunkConnection,
pad: *mut u16,
@@ -2706,7 +2706,7 @@ pub const PUNKTFUNK_RUMBLE_NO_TTL: u32 = 0xFFFF_FFFF;
/// `c` is a valid connection handle; out pointers are writable (NULLs are skipped). At most one
/// thread pulls rumble — it may run concurrently with the video/audio pullers.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_next_rumble2(
c: *mut PunktfunkConnection,
pad: *mut u16,
@@ -2786,7 +2786,7 @@ pub const PUNKTFUNK_RUMBLE_QUIRK_DEDUP_JITTER: u32 = 1;
/// `c` is a valid connection handle; out pointers are writable (NULLs are skipped). At most one
/// thread pulls rumble — it may run concurrently with the video/audio pullers.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_next_rumble_cmd(
c: *mut PunktfunkConnection,
pad: *mut u16,
@@ -2867,7 +2867,7 @@ pub unsafe extern "C" fn punktfunk_connection_next_rumble_cmd(
/// `c` is a valid connection handle; out pointers are writable (NULLs are skipped). At most one
/// thread pulls rumble — it may run concurrently with the video/audio pullers.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_next_rumble_cmd2(
c: *mut PunktfunkConnection,
pad: *mut u16,
@@ -2933,7 +2933,7 @@ pub unsafe extern "C" fn punktfunk_connection_next_rumble_cmd2(
/// # Safety
/// `c` is a valid connection handle. Callable from any thread.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_set_rumble_quirks(
c: *mut PunktfunkConnection,
pad: u16,
@@ -2970,7 +2970,7 @@ pub unsafe extern "C" fn punktfunk_connection_set_rumble_quirks(
/// # Safety
/// `c` is a valid connection handle; `out` is writable for one `PunktfunkHidOutput`.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_next_hidout(
c: *mut PunktfunkConnection,
out: *mut PunktfunkHidOutput,
@@ -3018,7 +3018,7 @@ pub unsafe extern "C" fn punktfunk_connection_next_hidout(
/// # Safety
/// `c` is a valid connection handle; `out` is writable for one `PunktfunkHdrMeta`.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_next_hdr_meta(
c: *mut PunktfunkConnection,
out: *mut PunktfunkHdrMeta,
@@ -3086,7 +3086,7 @@ pub struct PunktfunkCursorState {
/// `c` is a valid connection handle; `out` is writable. At most one thread pulls cursor
/// shapes; it may run concurrently with every other plane's puller.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_next_cursor_shape(
c: *mut PunktfunkConnection,
out: *mut PunktfunkCursorShape,
@@ -3138,7 +3138,7 @@ pub unsafe extern "C" fn punktfunk_connection_next_cursor_shape(
/// `c` is a valid connection handle; `out` is writable. At most one thread pulls cursor
/// state; it may run concurrently with every other plane's puller.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_next_cursor_state(
c: *mut PunktfunkConnection,
out: *mut PunktfunkCursorState,
@@ -3187,7 +3187,7 @@ pub unsafe extern "C" fn punktfunk_connection_next_cursor_state(
/// # Safety
/// `c` is a valid connection handle.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_set_cursor_render(
c: *mut PunktfunkConnection,
client_draws: bool,
@@ -3218,7 +3218,7 @@ pub unsafe extern "C" fn punktfunk_connection_set_cursor_render(
/// # Safety
/// `c` is a valid connection handle; `out` is writable for one `PunktfunkHostTiming`.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_next_host_timing(
c: *mut PunktfunkConnection,
out: *mut PunktfunkHostTiming,
@@ -3265,7 +3265,7 @@ pub unsafe extern "C" fn punktfunk_connection_next_host_timing(
/// # Safety
/// `c` is a valid connection handle; each out pointer is NULL or writable for its scalar.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_color_info(
c: *mut PunktfunkConnection,
primaries: *mut u8,
@@ -3315,7 +3315,7 @@ pub unsafe extern "C" fn punktfunk_connection_color_info(
/// # Safety
/// `c` is a valid connection handle; `out` is NULL or writable for one `u8`.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_chroma_format(
c: *mut PunktfunkConnection,
out: *mut u8,
@@ -3345,7 +3345,7 @@ pub unsafe extern "C" fn punktfunk_connection_chroma_format(
/// # Safety
/// `c` is a valid connection handle; `out` is NULL or writable for one `u8`.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_codec(
c: *mut PunktfunkConnection,
out: *mut u8,
@@ -3375,7 +3375,7 @@ pub unsafe extern "C" fn punktfunk_connection_codec(
/// # Safety
/// `c` is a valid connection handle; `out` is NULL or writable for one `u32`.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_shard_payload(
c: *mut PunktfunkConnection,
out: *mut u32,
@@ -3403,7 +3403,7 @@ pub unsafe extern "C" fn punktfunk_connection_shard_payload(
/// # Safety
/// `c` is a valid connection handle; `ev` points to a readable `InputEvent`-sized allocation.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_send_input(
c: *mut PunktfunkConnection,
ev: *const InputEvent,
@@ -3437,7 +3437,7 @@ pub unsafe extern "C" fn punktfunk_connection_send_input(
/// # Safety
/// `c` is a valid connection handle; `opus_data` is valid for `len` bytes (or `len == 0`).
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_send_mic(
c: *mut PunktfunkConnection,
opus_data: *const u8,
@@ -3478,7 +3478,7 @@ pub unsafe extern "C" fn punktfunk_connection_send_mic(
/// # Safety
/// `c` is a valid connection handle; `rich` points to a valid [`PunktfunkRichInput`].
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_send_rich_input(
c: *mut PunktfunkConnection,
rich: *const PunktfunkRichInput,
@@ -3516,7 +3516,7 @@ pub unsafe extern "C" fn punktfunk_connection_send_rich_input(
/// `c` is a valid connection handle; `rich` is null or points to at least its declared
/// `struct_size` bytes.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_send_rich_input2(
c: *mut PunktfunkConnection,
rich: *const PunktfunkRichInputEx,
@@ -3566,7 +3566,7 @@ pub unsafe extern "C" fn punktfunk_connection_send_rich_input2(
/// `c` is a valid connection handle; `samples` is null or points to `count` valid
/// [`PunktfunkPenSample`]s.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_send_pen(
c: *mut PunktfunkConnection,
samples: *const PunktfunkPenSample,
@@ -3609,7 +3609,7 @@ pub unsafe extern "C" fn punktfunk_connection_send_pen(
/// # Safety
/// `c` is a valid connection handle; out pointers are writable (NULLs are skipped).
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_mode(
c: *const PunktfunkConnection,
width: *mut u32,
@@ -3650,7 +3650,7 @@ pub unsafe extern "C" fn punktfunk_connection_mode(
/// # Safety
/// `c` is a valid connection handle; `gamepad` is writable (NULL is skipped).
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_gamepad(
c: *const PunktfunkConnection,
gamepad: *mut u32,
@@ -3835,7 +3835,7 @@ fn build_clip_event(
/// # Safety
/// `c` is a valid connection handle; `caps` is writable (NULL is skipped).
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_host_caps(
c: *const PunktfunkConnection,
caps: *mut u8,
@@ -3866,7 +3866,7 @@ pub unsafe extern "C" fn punktfunk_connection_host_caps(
/// # Safety
/// `c` is a valid connection handle.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_clipboard_control(
c: *const PunktfunkConnection,
enabled: bool,
@@ -3895,7 +3895,7 @@ pub unsafe extern "C" fn punktfunk_connection_clipboard_control(
/// `c` is a valid connection handle; `kinds` points to `n` `PunktfunkClipKind`s (NULL allowed only
/// when `n == 0`), each `mime` a valid NUL-terminated UTF-8 string.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_clipboard_offer(
c: *const PunktfunkConnection,
seq: u32,
@@ -3951,7 +3951,7 @@ pub unsafe extern "C" fn punktfunk_connection_clipboard_offer(
/// `c` is a valid connection handle; `mime` is a valid NUL-terminated UTF-8 string; `xfer_id_out`
/// is writable (NULL is skipped).
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_clipboard_fetch(
c: *const PunktfunkConnection,
seq: u32,
@@ -4000,7 +4000,7 @@ pub unsafe extern "C" fn punktfunk_connection_clipboard_fetch(
/// `c` is a valid connection handle; `data` points to `len` bytes (NULL allowed only when
/// `len == 0`).
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_clipboard_serve(
c: *const PunktfunkConnection,
req_id: u32,
@@ -4039,7 +4039,7 @@ pub unsafe extern "C" fn punktfunk_connection_clipboard_serve(
/// # Safety
/// `c` is a valid connection handle.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_clipboard_cancel(
c: *const PunktfunkConnection,
id: u32,
@@ -4067,7 +4067,7 @@ pub unsafe extern "C" fn punktfunk_connection_clipboard_cancel(
/// # Safety
/// `c` is a valid connection handle; `out` is writable for one `PunktfunkClipEvent`.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_next_clipboard(
c: *mut PunktfunkConnection,
out: *mut PunktfunkClipEvent,
@@ -4118,7 +4118,7 @@ pub unsafe extern "C" fn punktfunk_connection_next_clipboard(
/// # Safety
/// `c` is a valid connection handle; `compositor` is writable (NULL is skipped).
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_compositor(
c: *const PunktfunkConnection,
compositor: *mut u32,
@@ -4149,7 +4149,7 @@ pub unsafe extern "C" fn punktfunk_connection_compositor(
/// # Safety
/// `c` is a valid connection handle; `bitrate_kbps` is writable (NULL is skipped).
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_bitrate(
c: *const PunktfunkConnection,
bitrate_kbps: *mut u32,
@@ -4184,7 +4184,7 @@ pub unsafe extern "C" fn punktfunk_connection_bitrate(
/// # Safety
/// `c` is a valid connection handle; `offset_ns` is writable (NULL is skipped).
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_clock_offset_ns(
c: *const PunktfunkConnection,
offset_ns: *mut i64,
@@ -4218,7 +4218,7 @@ pub unsafe extern "C" fn punktfunk_connection_clock_offset_ns(
/// # Safety
/// `c` is a valid connection handle; `offset_ns` is writable (NULL is skipped).
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_clock_offset_now_ns(
c: *const PunktfunkConnection,
offset_ns: *mut i64,
@@ -4252,7 +4252,7 @@ pub unsafe extern "C" fn punktfunk_connection_clock_offset_now_ns(
/// # Safety
/// `c` is a valid connection handle.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_request_mode(
c: *const PunktfunkConnection,
width: u32,
@@ -4288,7 +4288,7 @@ pub unsafe extern "C" fn punktfunk_connection_request_mode(
/// # Safety
/// `c` is a valid connection handle.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_request_keyframe(
c: *const PunktfunkConnection,
) -> PunktfunkStatus {
@@ -4320,7 +4320,7 @@ pub unsafe extern "C" fn punktfunk_connection_request_keyframe(
/// # Safety
/// `c` is a valid connection handle.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_request_rfi(
c: *const PunktfunkConnection,
first_frame: u32,
@@ -4355,7 +4355,7 @@ pub unsafe extern "C" fn punktfunk_connection_request_rfi(
/// # Safety
/// `c` is a valid connection handle; `gap_out` is writable or NULL.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_note_frame_index(
c: *const PunktfunkConnection,
frame_index: u32,
@@ -4389,7 +4389,7 @@ pub unsafe extern "C" fn punktfunk_connection_note_frame_index(
/// # Safety
/// `c` is a valid connection handle; `gap_width_out` is writable or NULL.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_note_frame_index_ex(
c: *const PunktfunkConnection,
frame_index: u32,
@@ -4423,7 +4423,7 @@ pub unsafe extern "C" fn punktfunk_connection_note_frame_index_ex(
/// # Safety
/// `c` is a valid connection handle; `out` is writable (NULL is skipped).
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_frames_dropped(
c: *const PunktfunkConnection,
out: *mut u64,
@@ -4468,7 +4468,7 @@ pub unsafe extern "C" fn punktfunk_connection_frames_dropped(
/// # Safety
/// `c` is a valid connection handle.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_report_decode_us(
c: *const PunktfunkConnection,
us: u32,
@@ -4495,7 +4495,7 @@ pub unsafe extern "C" fn punktfunk_connection_report_decode_us(
/// `c` is an opaque handle from a `*_new`/`*_pair` the caller has not yet freed, or null (an
/// error, not UB).
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_report_phase(
c: *const PunktfunkConnection,
next_latch_host_ns: u64,
@@ -4531,7 +4531,7 @@ pub unsafe extern "C" fn punktfunk_connection_report_phase(
/// # Safety
/// `c` is a valid connection handle; `out` is writable (NULL is skipped).
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_wants_decode_latency(
c: *const PunktfunkConnection,
out: *mut bool,
@@ -4603,7 +4603,7 @@ pub struct PunktfunkProbeResult {
/// # Safety
/// `c` is a valid connection handle.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_speed_test(
c: *const PunktfunkConnection,
target_kbps: u32,
@@ -4631,7 +4631,7 @@ pub unsafe extern "C" fn punktfunk_connection_speed_test(
/// `c` is a valid connection handle; `out` is writable for one `PunktfunkProbeResult` (NULL is an
/// error).
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_probe_result(
c: *const PunktfunkConnection,
out: *mut PunktfunkProbeResult,
@@ -4678,7 +4678,7 @@ pub unsafe extern "C" fn punktfunk_connection_probe_result(
/// # Safety
/// `c` was returned by [`punktfunk_connect`] and remains valid (closed via `punktfunk_connection_close`).
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_disconnect_quit(c: *mut PunktfunkConnection) {
guard_void(|| {
// SAFETY: per the ABI contract - an opaque handle from a `*_new`/`*_pair` that the caller has
@@ -4695,7 +4695,7 @@ pub unsafe extern "C" fn punktfunk_connection_disconnect_quit(c: *mut PunktfunkC
/// # Safety
/// `c` was returned by [`punktfunk_connect`] and is not used after this call.
#[cfg(feature = "quic")]
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_connection_close(c: *mut PunktfunkConnection) {
guard_void(|| {
if !c.is_null() {
@@ -4721,7 +4721,7 @@ pub unsafe extern "C" fn punktfunk_connection_close(c: *mut PunktfunkConnection)
/// Create a re-anchor gate seeded with the session's current `frames_dropped` (so the first
/// [`punktfunk_reanchor_gate_poll`] doesn't read the baseline as a loss). Free with
/// [`punktfunk_reanchor_gate_free`]. Never returns NULL.
#[no_mangle]
#[unsafe(no_mangle)]
pub extern "C" fn punktfunk_reanchor_gate_new(frames_dropped: u64) -> *mut ReanchorGate {
Box::into_raw(Box::new(ReanchorGate::new(frames_dropped)))
}
@@ -4730,7 +4730,7 @@ pub extern "C" fn punktfunk_reanchor_gate_new(frames_dropped: u64) -> *mut Reanc
///
/// # Safety
/// `g` was returned by [`punktfunk_reanchor_gate_new`] and is not used after this call.
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_reanchor_gate_free(g: *mut ReanchorGate) {
guard_void(|| {
if !g.is_null() {
@@ -4746,7 +4746,7 @@ pub unsafe extern "C" fn punktfunk_reanchor_gate_free(g: *mut ReanchorGate) {
///
/// # Safety
/// `g` is a valid gate handle.
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_reanchor_gate_arm(g: *mut ReanchorGate) {
guard_void(|| {
// SAFETY: per the ABI contract - an opaque handle from a `*_new`/`*_pair` that the caller has
@@ -4768,7 +4768,7 @@ pub unsafe extern "C" fn punktfunk_reanchor_gate_arm(g: *mut ReanchorGate) {
///
/// # Safety
/// `g` is a valid gate handle.
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_reanchor_gate_arm_expecting_drops(
g: *mut ReanchorGate,
expected_drops: u64,
@@ -4791,7 +4791,7 @@ pub unsafe extern "C" fn punktfunk_reanchor_gate_arm_expecting_drops(
///
/// # Safety
/// `g` is a valid gate handle; `out_present` is writable or NULL.
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_reanchor_gate_on_decoded(
g: *mut ReanchorGate,
flags: u32,
@@ -4823,7 +4823,7 @@ pub unsafe extern "C" fn punktfunk_reanchor_gate_on_decoded(
///
/// # Safety
/// `g` is a valid gate handle; `out_request_kf` is writable or NULL.
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_reanchor_gate_on_no_output(
g: *mut ReanchorGate,
out_request_kf: *mut bool,
@@ -4852,7 +4852,7 @@ pub unsafe extern "C" fn punktfunk_reanchor_gate_on_no_output(
///
/// # Safety
/// `g` is a valid gate handle; `out_request_kf` is writable or NULL.
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_reanchor_gate_poll(
g: *mut ReanchorGate,
frames_dropped: u64,
@@ -4881,7 +4881,7 @@ pub unsafe extern "C" fn punktfunk_reanchor_gate_poll(
///
/// # Safety
/// `g` is a valid gate handle; `out_holding` is writable or NULL.
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn punktfunk_reanchor_gate_is_holding(
g: *const ReanchorGate,
out_holding: *mut bool,
@@ -176,9 +176,9 @@ impl DataPump {
let clock_offset_ns = pump_clock_offset.load(Ordering::Relaxed);
// An applied re-sync invalidates the staleness run measured under the OLD offset:
// reset the counters and re-arm the clock-based detector if a step had disarmed it.
let gen = pump_clock_gen.load(Ordering::Relaxed);
if gen != seen_clock_gen {
seen_clock_gen = gen;
let clock_gen = pump_clock_gen.load(Ordering::Relaxed);
if clock_gen != seen_clock_gen {
seen_clock_gen = clock_gen;
stale_since = None;
noop_clock_flushes = 0;
// Every OWD reading shifted with the offset — the standing-latency floor and
+1 -1
View File
@@ -162,7 +162,7 @@ impl ErasureCoder for Gf16Coder {
.iter()
.zip(have)
.enumerate()
.filter(|(_, (_, &h))| h)
.filter(|&(_, (_, &h))| h)
.map(|(i, (s, _))| (i, &**s))
.collect();
let restored = reed_solomon_simd::decode(
+12 -4
View File
@@ -417,13 +417,21 @@ mod tests {
/// --nocapture --test-threads=1`.
#[tokio::test]
#[ignore = "measurement: sets process env and takes ~15 s of wall clock"]
// Test-only exemption from the crate's `deny(unsafe_code)`: mutating the process
// environment is `unsafe` in edition 2024, and this measurement's env knob is the
// documented single-threaded kind.
#[allow(unsafe_code)]
async fn mtu_discovery_climbs_only_as_high_as_the_peer_advertises() {
async fn climb(server_jumbo: bool, client_jumbo: bool) -> (u16, u128) {
let set = |on: bool| {
if on {
std::env::set_var("PUNKTFUNK_JUMBO", "1");
} else {
std::env::remove_var("PUNKTFUNK_JUMBO");
// SAFETY: this `#[ignore]`d measurement is documented above to run alone with
// `--test-threads=1`, so no concurrent thread reads or writes the environment.
unsafe {
if on {
std::env::set_var("PUNKTFUNK_JUMBO", "1");
} else {
std::env::remove_var("PUNKTFUNK_JUMBO");
}
}
};
set(server_jumbo);
@@ -72,7 +72,7 @@ const _: () = {
};
#[cfg(target_vendor = "apple")]
extern "C" {
unsafe extern "C" {
/// Darwin batched receive: up to `cnt` datagrams in one syscall; returns the count received and
/// sets each `msg_datalen` to its byte length. Present in libSystem on all macOS/iOS.
fn recvmsg_x(
@@ -16,7 +16,7 @@ mod android_mmsg {
pub msg_hdr: libc::msghdr,
pub msg_len: libc::c_uint,
}
extern "C" {
unsafe extern "C" {
pub fn sendmmsg(
sockfd: libc::c_int,
msgvec: *mut mmsghdr,
+1 -1
View File
@@ -12,7 +12,7 @@
[package]
name = "punktfunk-encode-worker"
version.workspace = true
edition = "2021"
edition.workspace = true
rust-version.workspace = true
license = "MIT OR Apache-2.0"
description = "punktfunk PyroWave encode worker: owns the priority-elevated Vulkan device so the host never carries a capability."
+8 -3
View File
@@ -163,15 +163,20 @@ mod tests {
impl EnvGuard {
fn set(dir: &std::path::Path) -> EnvGuard {
let prev = std::env::var_os("PUNKTFUNK_CONFIG_DIR");
std::env::set_var("PUNKTFUNK_CONFIG_DIR", dir);
// SAFETY: only called by tests that hold CONFIG_DIR_TEST_LOCK, which serializes
// every test that writes or reads this variable across the whole test binary.
unsafe { std::env::set_var("PUNKTFUNK_CONFIG_DIR", dir) };
EnvGuard(prev)
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
match self.0.take() {
Some(v) => std::env::set_var("PUNKTFUNK_CONFIG_DIR", v),
None => std::env::remove_var("PUNKTFUNK_CONFIG_DIR"),
// SAFETY: dropped while the owning test still holds CONFIG_DIR_TEST_LOCK — the
// same serialization as `EnvGuard::set`.
Some(v) => unsafe { std::env::set_var("PUNKTFUNK_CONFIG_DIR", v) },
// SAFETY: as above.
None => unsafe { std::env::remove_var("PUNKTFUNK_CONFIG_DIR") },
}
}
}
+10 -4
View File
@@ -734,7 +734,9 @@ mod tests {
std::fs::create_dir_all(&dir).unwrap();
std::fs::create_dir_all(&outside).unwrap();
// Confine the proxy to `dir` for the duration of this test.
std::env::set_var("PUNKTFUNK_LIBRARY_ART_ROOTS", &dir);
// SAFETY: `_guard` holds ART_ROOTS_LOCK (`lock_art_roots`), which serializes every test
// that writes or reads this variable in the binary.
unsafe { std::env::set_var("PUNKTFUNK_LIBRARY_ART_ROOTS", &dir) };
// A real image inside the root: served, with the content type SNIFFED from the bytes.
let cover = dir.join("cover.png");
@@ -810,7 +812,8 @@ mod tests {
// A UNC path is refused outright (outbound SMB auth coercion), before any filesystem hit.
assert!(!art_path_is_servable(r"\\attacker\share\a.png"));
std::env::remove_var("PUNKTFUNK_LIBRARY_ART_ROOTS");
// SAFETY: still under `_guard` — the same ART_ROOTS_LOCK serialization as the set.
unsafe { std::env::remove_var("PUNKTFUNK_LIBRARY_ART_ROOTS") };
let _ = std::fs::remove_dir_all(&dir);
let _ = std::fs::remove_dir_all(&outside);
}
@@ -878,7 +881,9 @@ mod tests {
let outside = std::env::temp_dir().join(format!("pf-art-wr-out-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::create_dir_all(&outside).unwrap();
std::env::set_var("PUNKTFUNK_LIBRARY_ART_ROOTS", &dir);
// SAFETY: `_guard` holds ART_ROOTS_LOCK (`lock_art_roots`), which serializes every test
// that writes or reads this variable in the binary.
unsafe { std::env::set_var("PUNKTFUNK_LIBRARY_ART_ROOTS", &dir) };
let cover = dir.join("cover.png");
std::fs::write(&cover, PNG).unwrap();
@@ -930,7 +935,8 @@ mod tests {
"an out-of-root file:// cover is still refused"
);
std::env::remove_var("PUNKTFUNK_LIBRARY_ART_ROOTS");
// SAFETY: still under `_guard` — the same ART_ROOTS_LOCK serialization as the set.
unsafe { std::env::remove_var("PUNKTFUNK_LIBRARY_ART_ROOTS") };
let _ = std::fs::remove_dir_all(&dir);
let _ = std::fs::remove_dir_all(&outside);
}
+8 -4
View File
@@ -56,8 +56,10 @@ mod gamestream;
#[path = "linux/gpuclocks.rs"]
mod gpuclocks;
mod hooks;
// Network-facing on the secure default host (see the forbid block at `mod mgmt` below).
#[forbid(unsafe_code)]
// Network-facing on the secure default host (see the forbid block at `mod mgmt` below). Test
// builds carve out like `native`: the identity tests scope `PUNKTFUNK_CONFIG_DIR` via
// `env::set_var`, which edition 2024 makes an unsafe fn; shipped code keeps the forbid.
#[cfg_attr(not(test), forbid(unsafe_code))]
mod identity;
// The input-injection backends live in the `pf-inject` subsystem crate (plan §W6); this shim keeps
// every existing `crate::inject::*` path valid (the native/gamestream input planes + devtest consume
@@ -81,8 +83,10 @@ mod log_capture;
// exposes — are safe Rust by compiler-enforced invariant (rust-safety programme): `forbid`
// here means a future edit cannot quietly introduce unsafe into a network-facing module.
// (`native` carves out its `#[cfg(test)]` C-ABI roundtrip tests, which exercise the CLIENT
// side of punktfunk-core against this host in-process and are unsafe by nature.)
#[forbid(unsafe_code)]
// side of punktfunk-core against this host in-process and are unsafe by nature; `mgmt` and
// `identity` carve out test builds too — their tests scope `PUNKTFUNK_CONFIG_DIR` via
// `env::set_var`, an unsafe fn since edition 2024.)
#[cfg_attr(not(test), forbid(unsafe_code))]
mod mgmt;
#[forbid(unsafe_code)]
mod mgmt_token;
+9 -6
View File
@@ -787,8 +787,11 @@ async fn paired_clients_list_and_unpair() {
impl Drop for EnvGuard {
fn drop(&mut self) {
match self.0.take() {
Some(v) => std::env::set_var("PUNKTFUNK_CONFIG_DIR", v),
None => std::env::remove_var("PUNKTFUNK_CONFIG_DIR"),
// SAFETY: dropped while this test still holds CONFIG_DIR_TEST_LOCK, which
// serializes every test that writes or reads this variable in the binary.
Some(v) => unsafe { std::env::set_var("PUNKTFUNK_CONFIG_DIR", v) },
// SAFETY: as above.
None => unsafe { std::env::remove_var("PUNKTFUNK_CONFIG_DIR") },
}
}
}
@@ -797,7 +800,9 @@ async fn paired_clients_list_and_unpair() {
.unwrap_or_else(|e| e.into_inner());
let tmp = tempfile::tempdir().unwrap();
let _env = EnvGuard(std::env::var_os("PUNKTFUNK_CONFIG_DIR"));
std::env::set_var("PUNKTFUNK_CONFIG_DIR", tmp.path());
// SAFETY: `_serial` holds CONFIG_DIR_TEST_LOCK (taken above), serializing every test that
// writes or reads this variable in the binary.
unsafe { std::env::set_var("PUNKTFUNK_CONFIG_DIR", tmp.path()) };
let state = test_state();
let app = test_app(state.clone(), None);
@@ -1363,9 +1368,7 @@ fn every_route_is_classified_for_the_plugin_and_cert_lanes() {
// 1. Every LIVE route has a classification row. A new route fails here until it gets one.
for (method, path) in &live {
assert!(
expected
.iter()
.any(|(m, p, _, _)| m == method && p == path),
expected.iter().any(|(m, p, _, _)| m == method && p == path),
"route {method} {path} has no lane classification — add a row to EXPECTED in this test \
and decide, deliberately, whether the plugin token and a paired streaming cert may \
reach it"
+10 -4
View File
@@ -2058,7 +2058,9 @@ mod tests {
"expected the open-loop pin, got {uncapped}"
);
// With the operator ceiling set, the Automatic pin is capped to the link rate...
std::env::set_var("PUNKTFUNK_PYROWAVE_MAX_MBPS", "4500");
// SAFETY: this test is the only writer of this variable in the process, so writers can't
// race each other; the only reader is `resolve_bitrate_kbps_for` on this same thread.
unsafe { std::env::set_var("PUNKTFUNK_PYROWAVE_MAX_MBPS", "4500") };
assert_eq!(
resolve_bitrate_kbps_for(Codec::PyroWave, 0, &mode, ChromaFormat::Yuv444, 10),
4_500_000
@@ -2078,7 +2080,8 @@ mod tests {
resolve_bitrate_kbps_for(Codec::PyroWave, 6_000_000, &mode, ChromaFormat::Yuv444, 10),
6_000_000
);
std::env::remove_var("PUNKTFUNK_PYROWAVE_MAX_MBPS");
// SAFETY: as the set above — single writer, and the readers run on this thread.
unsafe { std::env::remove_var("PUNKTFUNK_PYROWAVE_MAX_MBPS") };
}
#[test]
@@ -2434,13 +2437,16 @@ mod tests {
struct EnvGuard(&'static str);
impl Drop for EnvGuard {
fn drop(&mut self) {
std::env::remove_var(self.0);
// SAFETY: dropped while SESSION_TEST_LOCK is still held by the owning test, so
// env writers stay serialized and only the session path reads this variable.
unsafe { std::env::remove_var(self.0) };
}
}
let _env = EnvGuard("PUNKTFUNK_CLIPBOARD");
// Operator policy on. Session tests serialize on SESSION_TEST_LOCK, and only the session
// path (a session test) reads this env, so the mutation is race-free here.
std::env::set_var("PUNKTFUNK_CLIPBOARD", "1");
// SAFETY: see the serialization argument directly above.
unsafe { std::env::set_var("PUNKTFUNK_CLIPBOARD", "1") };
let host = std::thread::spawn(|| {
run_ephemeral(Punktfunk1Options {
+8 -3
View File
@@ -212,7 +212,10 @@ fn load_host_env() {
if let Some((k, v)) = line.split_once('=') {
let (k, v) = (k.trim(), v.trim().trim_matches('"'));
if !k.is_empty() {
std::env::set_var(k, v);
// SAFETY: called from the service main before this process spawns any thread —
// the network-profile warner and the supervisor's host child both start after
// `load_host_env` returns, so nothing reads the environment concurrently.
unsafe { std::env::set_var(k, v) };
n += 1;
}
}
@@ -325,11 +328,13 @@ fn run_service() -> Result<()> {
console (session 0)"
);
// BEFORE the warner thread below: `load_host_env` mutates the process env, and the warner
// spawns a child process (which snapshots the env block) — the write must not run beside it.
load_host_env();
// Best-effort: warn if this network is Public (streaming ports are firewalled off there unless
// the operator opted in). Own thread — a slow `Get-NetConnectionProfile` never delays the host.
std::thread::spawn(warn_if_public_network);
load_host_env();
let result = supervise(stop, session);
// Report STOPPED regardless of how supervise returned.
+3 -1
View File
@@ -372,7 +372,9 @@ fn notify_on_connect(hwnd: HWND) {
nid.hBalloonIcon = unsafe {
LoadImageW(
Some(GetModuleHandleW(None).unwrap_or_default().into()),
PCWSTR(1usize as *const u16),
// `without_provenance`, not `ptr::dangling()`: this is MAKEINTRESOURCE(1) — the
// ADDRESS is the resource ordinal, and dangling() would yield align_of::<u16>() = 2.
PCWSTR(std::ptr::without_provenance(1)),
IMAGE_ICON,
sm,
sm,
+5
View File
@@ -0,0 +1,5 @@
# This workspace has been edition 2024 (and formatted under the 2024 style edition) since
# before the main workspace migrated. The repo-root rustfmt.toml pins style_edition = "2021"
# to keep that migration's diff reviewable; without this file, config discovery would walk up
# to the root pin and demand a 2021-style reformat of this already-2024 tree.
style_edition = "2024"
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "pf-vkhdr-layer"
version = "0.1.0"
edition = "2021"
edition = "2024"
description = "punktfunk Vulkan implicit layer: inject HDR10/scRGB surface formats on the virtual display so Vulkan games (id Tech, etc.) detect HDR over an IddCx virtual display"
license = "MIT OR Apache-2.0"
publish = false
+8 -8
View File
@@ -380,7 +380,7 @@ mod hdr {
}
#[link(name = "user32")]
extern "system" {
unsafe extern "system" {
fn MonitorFromWindow(h: HWND, flags: u32) -> HMONITOR;
fn GetMonitorInfoW(h: HMONITOR, mi: *mut MonitorInfoExW) -> i32;
fn GetDisplayConfigBufferSizes(flags: u32, np: *mut u32, nm: *mut u32) -> i32;
@@ -513,7 +513,7 @@ fn should_inject(surface: vk::SurfaceKHR) -> bool {
/// `p` must be null or point to a live `NegotiateLayerInterface` the caller has exclusive access
/// to for the duration of the call. The Vulkan loader — the only intended caller of this
/// export — guarantees exactly that for the negotiate handshake.
#[no_mangle]
#[unsafe(no_mangle)]
pub unsafe extern "system" fn vkNegotiateLoaderLayerInterfaceVersion(
p: *mut NegotiateLayerInterface,
) -> vk::Result {
@@ -767,12 +767,12 @@ unsafe extern "system" fn destroy_instance(inst: vk::Instance, p_alloc: *const c
// SAFETY: `inst` is non-null, and vkDestroyInstance requires a live instance handle —
// still live during this call — whose first word is the dispatch key.
.and_then(|mut g| g.remove(&unsafe { key(inst.as_raw()) }));
if let Some(d) = data {
if let Some(f) = d.destroy_instance {
// SAFETY: `f` is the down-chain vkDestroyInstance resolved for this very instance at
// create time; forwarding the caller's own arguments unchanged.
unsafe { f(inst, p_alloc) };
}
if let Some(d) = data
&& let Some(f) = d.destroy_instance
{
// SAFETY: `f` is the down-chain vkDestroyInstance resolved for this very instance at
// create time; forwarding the caller's own arguments unchanged.
unsafe { f(inst, p_alloc) };
}
}
+8
View File
@@ -0,0 +1,8 @@
# Pin the STYLE edition independently of the language edition. Migrating the workspace to
# edition 2024 would otherwise flip rustfmt to the 2024 style edition in the same commit and
# reformat ~370 files nobody touched — noise that buries the real migration diff and makes a
# bisect useless. Adopting the 2024 style is fine, but it is its own one-line-plus-reformat
# commit, not a side effect of this one. (packaging/windows/drivers pins 2024 in its own
# rustfmt.toml — it was already formatted under that style; config discovery would otherwise
# walk up to this file.)
style_edition = "2021"
+10 -8
View File
@@ -22,12 +22,14 @@
# above the fn.
#
# C. Safe-but-process-global APIs: `env::set_var`/`remove_var`, `sigaction`, `setlocale`,
# `set_current_dir`. Each is safe to call and unsound (or racy) from a live multithreaded
# process — the 972af299 environ data race lived in a file with ZERO occurrences of the word
# `unsafe`, invisible to the census. Edition 2024 makes `env::set_var` unsafe; until that
# migration this count-ratchet is the control. The baseline below enumerates today's debt
# per file; ANY increase (or a new file) fails. Shrink a file's count? Lower its baseline in
# the same commit.
# `set_current_dir`. Each is (or was) callable without `unsafe` and unsound (or racy) from a
# live multithreaded process — the 972af299 environ data race lived in a file with ZERO
# occurrences of the word `unsafe`, invisible to the census. Since the edition-2024
# migration the env pair is `unsafe fn` (compiler-enforced, SAFETY proof per site, counted
# by the census); this ratchet stays for the still-safe APIs (`sigaction`, `setlocale`,
# `set_current_dir`) and as a growth brake on env mutation generally. The baseline below
# enumerates today's debt per file; ANY increase (or a new file) fails. Shrink a file's
# count? Lower its baseline in the same commit.
#
# All three gates were shown to FAIL on deliberately planted instances before being made blocking
# (the gate-of-the-gate rule that caught cd72f77a's `0 * SLOT`).
@@ -167,8 +169,8 @@ clients/linux/src/app.rs:1
clients/linux/src/spawn.rs:1
clients/session/src/main.rs:4
crates/pf-console-ui/src/screens/settings.rs:1
crates/pf-console-ui/src/shell/tests.rs:2
crates/pf-encode/src/enc/linux/nvenc_cuda.rs:49
crates/pf-console-ui/src/shell/tests.rs:1
crates/pf-encode/src/enc/linux/nvenc_cuda.rs:2
crates/pf-encode/src/enc/linux/worker.rs:1
crates/pf-encode/src/enc/windows/nvenc.rs:4
crates/pf-inject/src/inject/linux/steam_gadget.rs:5