Rust edition 2024: the whole tree, with the env-mutation class made visible (WP20) #177
+11
-1
@@ -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
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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 (~250–500 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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -268,7 +268,7 @@ fn flagged(flags: &[bool]) -> Vec<usize> {
|
||||
flags
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, &d)| d)
|
||||
.filter(|&(_, &d)| d)
|
||||
.map(|(i, _)| i)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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") },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user