From f373dffb5e8fe1f9b544556bddedfb468eb640e9 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 12 Aug 2026 15:22:56 +0200 Subject: [PATCH] chore: migrate the main workspace and pf-vkhdr-layer to edition 2024 (WP20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The safety half of the rust-safety programme's §8.4: `std::env::set_var`/`remove_var` are `unsafe fn` in edition 2024, converting the class of bug the programme found the hard way (the 972af299 environ data race lived in a file with ZERO occurrences of the word `unsafe`) from invisible to counted and compiler-enforced. Manifests: [workspace.package] edition 2021→2024, rust-version 1.82→1.85 (the pinned toolchain is 1.96.0, so no toolchain bump — only the declared floor rises); the 13 crates pinning `edition = "2021"` literally now inherit it (Trap 1: the root bump alone reaches only `edition.workspace = true` crates and would have left pf-encode/pf-capture/pf-inject et al. on 2021 while reading as complete); pf-driver-proto's stale rust-version 1.82 pin now inherits; pf-vkhdr-layer (a separate workspace, inherits nothing) bumped to 2024. The four vendored crates (fec-rs, cros-codecs, usbip-sim, the patched ndk) stay on 2021 deliberately — upstream code stays pristine. The excluded usbip-poc standalone PoC is untouched. Mechanical, done textually across ALL cfg branches so no platform's half is left behind (Trap 3 — 44% of the host's unsafe is Windows-only and a one-platform `cargo fix` misses it): 148 `#[no_mangle]` → `#[unsafe(no_mangle)]` (83 in abi.rs); 12 bare extern blocks → `unsafe extern`; `gen` is a reserved keyword, so pf-vdisplay's generation stamps (registry.rs, windows/manager.rs) and the WinUI shell's animation counters rename gen → generation (internal identifiers only, no serde/wire surface); two match-ergonomics patterns take the compiler's suggested reference form. env mutation: every `set_var`/`remove_var` site (20 files) now sits in an `unsafe` block whose SAFETY comment states the real serialization argument (pf-vdisplay's ENV_LOCK, CONFIG_DIR_TEST_LOCK, ART_ROOTS_LOCK, vkdecode's gpu_lock, the `--test-threads=1` contracts of the hardware spikes, or single-threaded startup). Two genuine hazards surfaced en route — exactly the WP3b-class finds this migration exists to make visible — and are fixed here: - windows/service.rs spawned the network-profile warner thread BEFORE `load_host_env()`, so a child-spawning thread (child spawn snapshots the env block) was live while `set_var` ran in a loop; the load now precedes the spawn. - pf-console-ui's `fake_home()` re-set HOME outside its OnceLock on EVERY call, so two parallel tests could race the write; the set now happens exactly once inside `get_or_init`. cbindgen (Trap 2): 0.29.4 parses `#[unsafe(no_mangle)]` — verified empirically; the header regenerates byte-identical. The ci.yml drift check could never catch "failed to regenerate" (build.rs demotes a cbindgen failure to a warning and writes nothing, leaving the checked-in header untouched and the diff clean), so the step now first asserts the "punktfunk-core: wrote" line and the absence of "cbindgen failed" (sh -e safe: no `!` pipeline, no tee-masked exit). rustfmt: style_edition pinned to 2021 at the root — edition 2024 would otherwise flip the style edition and reformat ~370 untouched files inside this same commit, burying the migration diff. The drivers workspace pins its already-current 2024 style. Adopting the 2024 style tree-wide is its own future one-line-plus-reformat commit. Census: the primary metric moves UP BY DESIGN — 2435 → 2453 operations, unsafe blocks 1534 → 1577, and env_set_var is now a counted category (45 ops). The newly counted env sites are a truer number, not a regression; baseline snapshot saved as punktfunk-planning design/rust-safety-census-baseline-2026-08-12-edition-2024.txt. Gate C's env ratchet is now compiler-enforced (the hygiene-script header says so); the two shrunk file counts (nvenc_cuda 49→2 via the test helpers, shell/tests 2→1) are lowered in the same commit per the gate's own rule. Drop order (the semantic change most likely to bite this codebase): the migration lint `-W tail-expr-drop-order` reports zero findings on the macOS-visible halves of pf-encode / pf-zerocopy / pf-capture / pf-frame; the Linux and Windows halves run the same lint on the gate boxes. The four #[ignore]d alloc/drop-cycle tests on the hardware boxes remain owed, as before this change. --- .gitea/workflows/ci.yml | 12 +- Cargo.toml | 4 +- clients/android/native/src/discovery.rs | 6 +- clients/android/native/src/feedback.rs | 4 +- clients/android/native/src/lib.rs | 6 +- clients/android/native/src/probe.rs | 2 +- .../android/native/src/session/clipboard.rs | 14 +- clients/android/native/src/session/connect.rs | 20 +- clients/android/native/src/session/input.rs | 36 +-- clients/android/native/src/session/planes.rs | 34 +-- clients/android/native/src/session/probe.rs | 4 +- clients/android/native/src/wol.rs | 2 +- clients/linux/src/app.rs | 4 +- clients/linux/src/spawn.rs | 5 +- clients/session/src/main.rs | 18 +- clients/windows/Cargo.toml | 2 +- clients/windows/src/app/hosts.rs | 20 +- clients/windows/src/app/mod.rs | 77 +++--- clients/windows/src/main.rs | 9 +- crates/pf-capture/Cargo.toml | 2 +- crates/pf-capture/src/windows/dxgi.rs | 2 +- crates/pf-client-core/src/overlay_focus.rs | 8 +- .../pf-client-core/src/video_d3d11_native.rs | 5 +- crates/pf-clipboard/Cargo.toml | 2 +- crates/pf-console-ui/src/screens/settings.rs | 12 +- crates/pf-console-ui/src/shell/tests.rs | 11 +- crates/pf-driver-proto/Cargo.toml | 4 +- crates/pf-encode/Cargo.toml | 2 +- crates/pf-encode/src/enc/linux/nvenc_cuda.rs | 132 ++++++---- crates/pf-encode/src/enc/linux/worker.rs | 8 +- crates/pf-encode/src/enc/windows/nvenc.rs | 19 +- crates/pf-frame/Cargo.toml | 2 +- crates/pf-frame/src/session_tuning.rs | 8 +- crates/pf-gpu/Cargo.toml | 2 +- crates/pf-gpu/src/lib.rs | 56 ++-- crates/pf-host-config/Cargo.toml | 2 +- crates/pf-host-config/src/lib.rs | 20 +- crates/pf-inject/Cargo.toml | 2 +- crates/pf-paths/Cargo.toml | 2 +- crates/pf-update/src/main.rs | 5 +- crates/pf-vdisplay/Cargo.toml | 2 +- crates/pf-vdisplay/src/vdisplay/registry.rs | 248 +++++++++--------- crates/pf-vdisplay/src/vdisplay/routing.rs | 15 +- crates/pf-vdisplay/src/vdisplay/session.rs | 78 +++--- .../src/vdisplay/windows/manager.rs | 134 +++++----- crates/pf-vkdecode/tests/fault_detection.rs | 2 +- crates/pf-vkdecode/tests/gpu_parity.rs | 20 +- crates/pf-win-display/Cargo.toml | 2 +- crates/pf-win-display/src/win_display.rs | 19 +- crates/pf-zerocopy/Cargo.toml | 2 +- crates/pf-zerocopy/src/imp/egl/gl.rs | 4 +- crates/punktfunk-core/src/abi.rs | 166 ++++++------ crates/punktfunk-core/src/client/pump/data.rs | 6 +- crates/punktfunk-core/src/fec/gf16.rs | 2 +- crates/punktfunk-core/src/quic/endpoint.rs | 16 +- .../punktfunk-core/src/transport/udp/apple.rs | 2 +- .../punktfunk-core/src/transport/udp/linux.rs | 2 +- crates/punktfunk-encode-worker/Cargo.toml | 2 +- crates/punktfunk-host/src/identity.rs | 11 +- crates/punktfunk-host/src/library/art.rs | 14 +- crates/punktfunk-host/src/main.rs | 12 +- crates/punktfunk-host/src/mgmt/tests.rs | 15 +- crates/punktfunk-host/src/native.rs | 14 +- crates/punktfunk-host/src/windows/service.rs | 11 +- crates/punktfunk-tray/src/win.rs | 4 +- packaging/windows/drivers/rustfmt.toml | 5 + packaging/windows/pf-vkhdr-layer/Cargo.toml | 2 +- packaging/windows/pf-vkhdr-layer/src/lib.rs | 16 +- rustfmt.toml | 8 + scripts/ci/check-unsafe-hygiene.sh | 18 +- 70 files changed, 801 insertions(+), 636 deletions(-) create mode 100644 packaging/windows/drivers/rustfmt.toml create mode 100644 rustfmt.toml diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 6a824cc6..fdc8db2f 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -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) diff --git a/Cargo.toml b/Cargo.toml index 97e4ef20..e31ab52c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/clients/android/native/src/discovery.rs b/clients/android/native/src/discovery.rs index 501df474..7c2f0fa7 100644 --- a/clients/android/native/src/discovery.rs +++ b/clients/android/native/src/discovery.rs @@ -200,7 +200,7 @@ fn resolve(info: &ResolvedService) -> Option { /// 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, diff --git a/clients/android/native/src/feedback.rs b/clients/android/native/src/feedback.rs index bca2ff5d..5b97ad37 100644 --- a/clients/android/native/src/feedback.rs +++ b/clients/android/native/src/feedback.rs @@ -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, diff --git a/clients/android/native/src/lib.rs b/clients/android/native/src/lib.rs index cc156155..1ed2706e 100644 --- a/clients/android/native/src/lib.rs +++ b/clients/android/native/src/lib.rs @@ -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>, diff --git a/clients/android/native/src/probe.rs b/clients/android/native/src/probe.rs index db6a37a1..5e24bd6f 100644 --- a/clients/android/native/src/probe.rs +++ b/clients/android/native/src/probe.rs @@ -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>, diff --git a/clients/android/native/src/session/clipboard.rs b/clients/android/native/src/session/clipboard.rs index 7309ac97..1d12db42 100644 --- a/clients/android/native/src/session/clipboard.rs +++ b/clients/android/native/src/session/clipboard.rs @@ -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::` 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, diff --git a/clients/android/native/src/session/connect.rs b/clients/android/native/src/session/connect.rs index 42ec297d..0b3fdf1d 100644 --- a/clients/android/native/src/session/connect.rs +++ b/clients/android/native/src/session/connect.rs @@ -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 `"\n-----PUNKTFUNK-KEY-----\n"`, 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>, diff --git a/clients/android/native/src/session/input.rs b/clients/android/native/src/session/input.rs index d5804b1f..a2a051fb 100644 --- a/clients/android/native/src/session/input.rs +++ b/clients/android/native/src/session/input.rs @@ -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, diff --git a/clients/android/native/src/session/planes.rs b/clients/android/native/src/session/planes.rs index 5a62288a..0ca4c80d 100644 --- a/clients/android/native/src/session/planes.rs +++ b/clients/android/native/src/session/planes.rs @@ -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, diff --git a/clients/android/native/src/session/probe.rs b/clients/android/native/src/session/probe.rs index 62125c2d..d3388497 100644 --- a/clients/android/native/src/session/probe.rs +++ b/clients/android/native/src/session/probe.rs @@ -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>, diff --git a/clients/android/native/src/wol.rs b/clients/android/native/src/wol.rs index 8b542991..a37e95fd 100644 --- a/clients/android/native/src/wol.rs +++ b/clients/android/native/src/wol.rs @@ -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>, diff --git a/clients/linux/src/app.rs b/clients/linux/src/app.rs index c8bb8ae3..769bdbbb 100644 --- a/clients/linux/src/app.rs +++ b/clients/linux/src/app.rs @@ -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). diff --git a/clients/linux/src/spawn.rs b/clients/linux/src/spawn.rs index f74be5ff..ecc4393b 100644 --- a/clients/linux/src/spawn.rs +++ b/clients/linux/src/spawn.rs @@ -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. diff --git a/clients/session/src/main.rs b/clients/session/src/main.rs index 0c3e804f..6109976e 100644 --- a/clients/session/src/main.rs +++ b/clients/session/src/main.rs @@ -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) }; } } diff --git a/clients/windows/Cargo.toml b/clients/windows/Cargo.toml index a3d1ad77..d84a0d21 100644 --- a/clients/windows/Cargo.toml +++ b/clients/windows/Cargo.toml @@ -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 diff --git a/clients/windows/src/app/hosts.rs b/clients/windows/src/app/hosts.rs index 81ac236e..801c2541 100644 --- a/clients/windows/src/app/hosts.rs +++ b/clients/windows/src/app/hosts.rs @@ -318,10 +318,10 @@ fn edit_editor( if !addr.is_empty() { h.addr = addr; } - if let Ok(p) = port_draft.borrow().trim().parse::() { - if p != 0 { - h.port = p; - } + if let Ok(p) = port_draft.borrow().trim().parse::() + && 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 }) diff --git a/clients/windows/src/app/mod.rs b/clients/windows/src/app/mod.rs index 9a9b2c89..a8f8b2f0 100644 --- a/clients/windows/src/app/mod.rs +++ b/clients/windows/src/app/mod.rs @@ -515,35 +515,37 @@ fn root(cx: &mut RenderCx, ctx: &Arc) -> 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 = - 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 = + 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) -> 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::::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) -> 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) -> 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) -> 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); diff --git a/clients/windows/src/main.rs b/clients/windows/src/main.rs index 278c3a94..0daddd82 100644 --- a/clients/windows/src/main.rs +++ b/clients/windows/src/main.rs @@ -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") { diff --git a/crates/pf-capture/Cargo.toml b/crates/pf-capture/Cargo.toml index 2cf59b01..1b30bb3c 100644 --- a/crates/pf-capture/Cargo.toml +++ b/crates/pf-capture/Cargo.toml @@ -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." diff --git a/crates/pf-capture/src/windows/dxgi.rs b/crates/pf-capture/src/windows/dxgi.rs index 962b22b9..0cbe46f5 100644 --- a/crates/pf-capture/src/windows/dxgi.rs +++ b/crates/pf-capture/src/windows/dxgi.rs @@ -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; } diff --git a/crates/pf-client-core/src/overlay_focus.rs b/crates/pf-client-core/src/overlay_focus.rs index 6ef294fc..b2401a12 100644 --- a/crates/pf-client-core/src/overlay_focus.rs +++ b/crates/pf-client-core/src/overlay_focus.rs @@ -164,10 +164,10 @@ fn read_appid(conn: &RustConnection, root: Window, atom: Atom) -> Option { .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 diff --git a/crates/pf-client-core/src/video_d3d11_native.rs b/crates/pf-client-core/src/video_d3d11_native.rs index 408d7990..334515fd 100644 --- a/crates/pf-client-core/src/video_d3d11_native.rs +++ b/crates/pf-client-core/src/video_d3d11_native.rs @@ -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 + } } } diff --git a/crates/pf-clipboard/Cargo.toml b/crates/pf-clipboard/Cargo.toml index 62cd2a71..8636866f 100644 --- a/crates/pf-clipboard/Cargo.toml +++ b/crates/pf-clipboard/Cargo.toml @@ -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." diff --git a/crates/pf-console-ui/src/screens/settings.rs b/crates/pf-console-ui/src/screens/settings.rs index 4f495763..7da7aa31 100644 --- a/crates/pf-console-ui/src/screens/settings.rs +++ b/crates/pf-console-ui/src/screens/settings.rs @@ -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 = 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 diff --git a/crates/pf-console-ui/src/shell/tests.rs b/crates/pf-console-ui/src/shell/tests.rs index 0e8898f9..84e500a8 100644 --- a/crates/pf-console-ui/src/shell/tests.rs +++ b/crates/pf-console-ui/src/shell/tests.rs @@ -49,13 +49,16 @@ fn motion_matches_the_shared_vectors() { fn fake_home() { use std::sync::OnceLock; static HOME: OnceLock = 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 { diff --git a/crates/pf-driver-proto/Cargo.toml b/crates/pf-driver-proto/Cargo.toml index f814fa23..2e5be95b 100644 --- a/crates/pf-driver-proto/Cargo.toml +++ b/crates/pf-driver-proto/Cargo.toml @@ -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 diff --git a/crates/pf-encode/Cargo.toml b/crates/pf-encode/Cargo.toml index eece77c3..86e73552 100644 --- a/crates/pf-encode/Cargo.toml +++ b/crates/pf-encode/Cargo.toml @@ -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." diff --git a/crates/pf-encode/src/enc/linux/nvenc_cuda.rs b/crates/pf-encode/src/enc/linux/nvenc_cuda.rs index a8999802..6d2d8617 100644 --- a/crates/pf-encode/src/enc/linux/nvenc_cuda.rs +++ b/crates/pf-encode/src/enc/linux/nvenc_cuda.rs @@ -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) { + // 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| -> (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 = (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 = (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 = (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 = (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"); diff --git a/crates/pf-encode/src/enc/linux/worker.rs b/crates/pf-encode/src/enc/linux/worker.rs index f76f3fc4..e31c8a51 100644 --- a/crates/pf-encode/src/enc/linux/worker.rs +++ b/crates/pf-encode/src/enc/linux/worker.rs @@ -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) }; } } diff --git a/crates/pf-encode/src/enc/windows/nvenc.rs b/crates/pf-encode/src/enc/windows/nvenc.rs index f2269e25..dffa2e12 100644 --- a/crates/pf-encode/src/enc/windows/nvenc.rs +++ b/crates/pf-encode/src/enc/windows/nvenc.rs @@ -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 diff --git a/crates/pf-frame/Cargo.toml b/crates/pf-frame/Cargo.toml index d388cfe1..15d3fcf9 100644 --- a/crates/pf-frame/Cargo.toml +++ b/crates/pf-frame/Cargo.toml @@ -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." diff --git a/crates/pf-frame/src/session_tuning.rs b/crates/pf-frame/src/session_tuning.rs index 0695fcbc..4616a13e 100644 --- a/crates/pf-frame/src/session_tuning.rs +++ b/crates/pf-frame/src/session_tuning.rs @@ -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; } diff --git a/crates/pf-gpu/Cargo.toml b/crates/pf-gpu/Cargo.toml index 9602dfc1..53008b54 100644 --- a/crates/pf-gpu/Cargo.toml +++ b/crates/pf-gpu/Cargo.toml @@ -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 diff --git a/crates/pf-gpu/src/lib.rs b/crates/pf-gpu/src/lib.rs index 241b38d2..bdcb18a6 100644 --- a/crates/pf-gpu/src/lib.rs +++ b/crates/pf-gpu/src/lib.rs @@ -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 { 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 { } 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 { /// (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() diff --git a/crates/pf-host-config/Cargo.toml b/crates/pf-host-config/Cargo.toml index 0cecefa6..514ab1c7 100644 --- a/crates/pf-host-config/Cargo.toml +++ b/crates/pf-host-config/Cargo.toml @@ -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 diff --git a/crates/pf-host-config/src/lib.rs b/crates/pf-host-config/src/lib.rs index fcdb4b9e..9217e889 100644 --- a/crates/pf-host-config/src/lib.rs +++ b/crates/pf-host-config/src/lib.rs @@ -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; diff --git a/crates/pf-inject/Cargo.toml b/crates/pf-inject/Cargo.toml index 3dc7d463..24becf96 100644 --- a/crates/pf-inject/Cargo.toml +++ b/crates/pf-inject/Cargo.toml @@ -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." diff --git a/crates/pf-paths/Cargo.toml b/crates/pf-paths/Cargo.toml index e6b05887..3651ae7f 100644 --- a/crates/pf-paths/Cargo.toml +++ b/crates/pf-paths/Cargo.toml @@ -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 diff --git a/crates/pf-update/src/main.rs b/crates/pf-update/src/main.rs index 0525da04..405b672c 100644 --- a/crates/pf-update/src/main.rs +++ b/crates/pf-update/src/main.rs @@ -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; } diff --git a/crates/pf-vdisplay/Cargo.toml b/crates/pf-vdisplay/Cargo.toml index ad571f6f..2b167cf6 100644 --- a/crates/pf-vdisplay/Cargo.toml +++ b/crates/pf-vdisplay/Cargo.toml @@ -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." diff --git a/crates/pf-vdisplay/src/vdisplay/registry.rs b/crates/pf-vdisplay/src/vdisplay/registry.rs index 1aa5b12f..7f3d32f9 100644 --- a/crates/pf-vdisplay/src/vdisplay/registry.rs +++ b/crates/pf-vdisplay/src/vdisplay/registry.rs @@ -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) -> 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) -> 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, ) -> 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, ) -> Option { 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, @@ -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#`, one per dedicated session) would + /// the map bounded: the per-spawn keys (`gamescope#`, 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, @@ -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 { use crate::layout::{self, Member}; - let mut keys: Vec = rows.iter().map(|r| group_key(r.backend, r.gen)).collect(); + let mut keys: Vec = rows + .iter() + .map(|r| group_key(r.backend, r.generation)) + .collect(); keys.sort(); keys.dedup(); let mut out: Vec = 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 = 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 = 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) -> Entry { + /// `hand_off_restore` logic only reads `backend` + `generation` + `topology_restore`). + fn test_entry(backend: &'static str, generation: u64, restore: Option) -> 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, next: &mut u32, rows: &[Row]) { - let mut keys: Vec = rows.iter().map(|r| group_key(r.backend, r.gen)).collect(); + let mut keys: Vec = 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) -> Row { + fn row(generation: u64, backend: &'static str, w: u32, slot: Option) -> 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 = expired.iter().map(|e| e.gen).collect(); + let gone: Vec = expired.iter().map(|e| e.generation).collect(); assert_eq!(gone, vec![1, 3]); - let left: Vec = es.iter().map(|e| e.gen).collect(); + let left: Vec = es.iter().map(|e| e.generation).collect(); assert_eq!(left, vec![2, 4, 5]); } } @@ -974,7 +981,7 @@ mod linux { struct Reg { entries: Mutex>, - gen: AtomicU64, + generation: AtomicU64, } static REG: OnceLock = 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, 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 = rows.iter().map(|r| group_key(r.backend, r.gen)).collect(); + let mut keys: Vec = rows + .iter() + .map(|r| group_key(r.backend, r.generation)) + .collect(); keys.sort(); keys.dedup(); static GROUP_IDS: Mutex, 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, 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)); } } } diff --git a/crates/pf-vdisplay/src/vdisplay/routing.rs b/crates/pf-vdisplay/src/vdisplay/routing.rs index f31ed4e6..f4ced37f 100644 --- a/crates/pf-vdisplay/src/vdisplay/routing.rs +++ b/crates/pf-vdisplay/src/vdisplay/routing.rs @@ -161,7 +161,10 @@ pub fn apply_input_env(chosen: Compositor, dedicated_launch: bool) -> Option "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, diff --git a/crates/pf-vdisplay/src/vdisplay/session.rs b/crates/pf-vdisplay/src/vdisplay/session.rs index 487e72dd..f6ad35a2 100644 --- a/crates/pf-vdisplay/src/vdisplay/session.rs +++ b/crates/pf-vdisplay/src/vdisplay/session.rs @@ -585,41 +585,49 @@ fn find_wayland_socket(env: &EnvProbe, runtime: &str, uid: u32) -> Option 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 diff --git a/crates/pf-vdisplay/src/vdisplay/windows/manager.rs b/crates/pf-vdisplay/src/vdisplay/windows/manager.rs index 049da9da..94ad53ce 100644 --- a/crates/pf-vdisplay/src/vdisplay/windows/manager.rs +++ b/crates/pf-vdisplay/src/vdisplay/windows/manager.rs @@ -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 { @@ -367,10 +367,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 { 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() } } @@ -425,7 +425,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, /// 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 @@ -473,7 +473,7 @@ pub(crate) fn init(driver: Box) -> &'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()), @@ -700,7 +700,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 @@ -746,7 +746,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)) @@ -807,7 +807,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; @@ -858,7 +858,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, @@ -875,7 +875,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( @@ -965,7 +965,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( @@ -981,7 +981,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 @@ -1211,18 +1211,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 = ordered .iter() .map(|&(slot, _, _, width)| Member { @@ -1553,7 +1553,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, }) @@ -1677,7 +1677,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 @@ -1821,8 +1821,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, @@ -1834,7 +1834,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). @@ -2023,10 +2023,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 { @@ -2135,7 +2135,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) { @@ -2195,38 +2195,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 = 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 = 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) }; + } } } }) @@ -2240,7 +2242,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 @@ -2251,7 +2253,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); } } @@ -2319,7 +2321,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). @@ -2327,7 +2329,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 { let inner = self.state.lock().unwrap(); @@ -2351,20 +2353,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) -> usize { @@ -2377,7 +2379,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) } @@ -2409,7 +2411,7 @@ pub(crate) fn snapshot() -> Vec { .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) -> usize { VDM.get().map(|m| m.force_release(slot)).unwrap_or(0) diff --git a/crates/pf-vkdecode/tests/fault_detection.rs b/crates/pf-vkdecode/tests/fault_detection.rs index 5a776c3b..33fea6c4 100644 --- a/crates/pf-vkdecode/tests/fault_detection.rs +++ b/crates/pf-vkdecode/tests/fault_detection.rs @@ -268,7 +268,7 @@ fn flagged(flags: &[bool]) -> Vec { flags .iter() .enumerate() - .filter(|(_, &d)| d) + .filter(|&(_, &d)| d) .map(|(i, _)| i) .collect() } diff --git a/crates/pf-vkdecode/tests/gpu_parity.rs b/crates/pf-vkdecode/tests/gpu_parity.rs index d4675f19..8c0d3820 100644 --- a/crates/pf-vkdecode/tests/gpu_parity.rs +++ b/crates/pf-vkdecode/tests/gpu_parity.rs @@ -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); diff --git a/crates/pf-win-display/Cargo.toml b/crates/pf-win-display/Cargo.toml index 6b8142ce..c69bd35b 100644 --- a/crates/pf-win-display/Cargo.toml +++ b/crates/pf-win-display/Cargo.toml @@ -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." diff --git a/crates/pf-win-display/src/win_display.rs b/crates/pf-win-display/src/win_display.rs index 80cbd8d7..5239c833 100644 --- a/crates/pf-win-display/src/win_display.rs +++ b/crates/pf-win-display/src/win_display.rs @@ -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 { // 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 { ) }); 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)", diff --git a/crates/pf-zerocopy/Cargo.toml b/crates/pf-zerocopy/Cargo.toml index 6ad8048e..16df1add 100644 --- a/crates/pf-zerocopy/Cargo.toml +++ b/crates/pf-zerocopy/Cargo.toml @@ -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 diff --git a/crates/pf-zerocopy/src/imp/egl/gl.rs b/crates/pf-zerocopy/src/imp/egl/gl.rs index b2062fdf..faa7b53f 100644 --- a/crates/pf-zerocopy/src/imp/egl/gl.rs +++ b/crates/pf-zerocopy/src/imp/egl/gl.rs @@ -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); } diff --git a/crates/punktfunk-core/src/abi.rs b/crates/punktfunk-core/src/abi.rs index 2982490f..32fc5440 100644 --- a/crates/punktfunk-core/src/abi.rs +++ b/crates/punktfunk-core/src/abi.rs @@ -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` (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; `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, @@ -4434,7 +4434,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, @@ -4461,7 +4461,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, @@ -4497,7 +4497,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, @@ -4569,7 +4569,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, @@ -4597,7 +4597,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, @@ -4644,7 +4644,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 @@ -4661,7 +4661,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() { @@ -4687,7 +4687,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))) } @@ -4696,7 +4696,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() { @@ -4712,7 +4712,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 @@ -4732,7 +4732,7 @@ pub unsafe extern "C" fn punktfunk_reanchor_gate_arm(g: *mut ReanchorGate) { /// /// # 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, @@ -4764,7 +4764,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, @@ -4793,7 +4793,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, @@ -4822,7 +4822,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, diff --git a/crates/punktfunk-core/src/client/pump/data.rs b/crates/punktfunk-core/src/client/pump/data.rs index 95b2c4e7..6457b23c 100644 --- a/crates/punktfunk-core/src/client/pump/data.rs +++ b/crates/punktfunk-core/src/client/pump/data.rs @@ -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 diff --git a/crates/punktfunk-core/src/fec/gf16.rs b/crates/punktfunk-core/src/fec/gf16.rs index e67ab210..13cfc636 100644 --- a/crates/punktfunk-core/src/fec/gf16.rs +++ b/crates/punktfunk-core/src/fec/gf16.rs @@ -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( diff --git a/crates/punktfunk-core/src/quic/endpoint.rs b/crates/punktfunk-core/src/quic/endpoint.rs index fa810d86..7fd2c4bd 100644 --- a/crates/punktfunk-core/src/quic/endpoint.rs +++ b/crates/punktfunk-core/src/quic/endpoint.rs @@ -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); diff --git a/crates/punktfunk-core/src/transport/udp/apple.rs b/crates/punktfunk-core/src/transport/udp/apple.rs index ea456813..29a0efce 100644 --- a/crates/punktfunk-core/src/transport/udp/apple.rs +++ b/crates/punktfunk-core/src/transport/udp/apple.rs @@ -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( diff --git a/crates/punktfunk-core/src/transport/udp/linux.rs b/crates/punktfunk-core/src/transport/udp/linux.rs index 5062acbb..847f6ea3 100644 --- a/crates/punktfunk-core/src/transport/udp/linux.rs +++ b/crates/punktfunk-core/src/transport/udp/linux.rs @@ -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, diff --git a/crates/punktfunk-encode-worker/Cargo.toml b/crates/punktfunk-encode-worker/Cargo.toml index 56d03943..134b83f0 100644 --- a/crates/punktfunk-encode-worker/Cargo.toml +++ b/crates/punktfunk-encode-worker/Cargo.toml @@ -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." diff --git a/crates/punktfunk-host/src/identity.rs b/crates/punktfunk-host/src/identity.rs index 694952c6..c486f92a 100644 --- a/crates/punktfunk-host/src/identity.rs +++ b/crates/punktfunk-host/src/identity.rs @@ -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") }, } } } diff --git a/crates/punktfunk-host/src/library/art.rs b/crates/punktfunk-host/src/library/art.rs index 7d6fa0ce..d5422627 100644 --- a/crates/punktfunk-host/src/library/art.rs +++ b/crates/punktfunk-host/src/library/art.rs @@ -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); } diff --git a/crates/punktfunk-host/src/main.rs b/crates/punktfunk-host/src/main.rs index 60022dd2..ae62404b 100644 --- a/crates/punktfunk-host/src/main.rs +++ b/crates/punktfunk-host/src/main.rs @@ -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; diff --git a/crates/punktfunk-host/src/mgmt/tests.rs b/crates/punktfunk-host/src/mgmt/tests.rs index dad4b379..70c9dee7 100644 --- a/crates/punktfunk-host/src/mgmt/tests.rs +++ b/crates/punktfunk-host/src/mgmt/tests.rs @@ -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" diff --git a/crates/punktfunk-host/src/native.rs b/crates/punktfunk-host/src/native.rs index f8d5b8c7..62684ef6 100644 --- a/crates/punktfunk-host/src/native.rs +++ b/crates/punktfunk-host/src/native.rs @@ -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 { diff --git a/crates/punktfunk-host/src/windows/service.rs b/crates/punktfunk-host/src/windows/service.rs index 8f0c1fab..de937d84 100644 --- a/crates/punktfunk-host/src/windows/service.rs +++ b/crates/punktfunk-host/src/windows/service.rs @@ -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. diff --git a/crates/punktfunk-tray/src/win.rs b/crates/punktfunk-tray/src/win.rs index 1b4976d2..24e0bfb4 100644 --- a/crates/punktfunk-tray/src/win.rs +++ b/crates/punktfunk-tray/src/win.rs @@ -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::() = 2. + PCWSTR(std::ptr::without_provenance(1)), IMAGE_ICON, sm, sm, diff --git a/packaging/windows/drivers/rustfmt.toml b/packaging/windows/drivers/rustfmt.toml new file mode 100644 index 00000000..375a515f --- /dev/null +++ b/packaging/windows/drivers/rustfmt.toml @@ -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" diff --git a/packaging/windows/pf-vkhdr-layer/Cargo.toml b/packaging/windows/pf-vkhdr-layer/Cargo.toml index bbd08740..4546e3cc 100644 --- a/packaging/windows/pf-vkhdr-layer/Cargo.toml +++ b/packaging/windows/pf-vkhdr-layer/Cargo.toml @@ -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 diff --git a/packaging/windows/pf-vkhdr-layer/src/lib.rs b/packaging/windows/pf-vkhdr-layer/src/lib.rs index 6be9f881..b4998fef 100644 --- a/packaging/windows/pf-vkhdr-layer/src/lib.rs +++ b/packaging/windows/pf-vkhdr-layer/src/lib.rs @@ -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) }; } } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..f44aac43 --- /dev/null +++ b/rustfmt.toml @@ -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" diff --git a/scripts/ci/check-unsafe-hygiene.sh b/scripts/ci/check-unsafe-hygiene.sh index 3fa03d4c..649d00f4 100755 --- a/scripts/ci/check-unsafe-hygiene.sh +++ b/scripts/ci/check-unsafe-hygiene.sh @@ -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 -- 2.54.0