diff --git a/Cargo.lock b/Cargo.lock index b9f7f52d..9a6ebe27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5429,9 +5429,9 @@ dependencies = [ [[package]] name = "wasapi" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80c3aa5d6b0e7acc3ea10cb19c334df0c8d825060f14a30d9e3b03385e6e5175" +checksum = "ee144c215d8d31e94c1a599fbaf540ca82a7017e4f1786816b179f334fac751d" dependencies = [ "log", "num-integer", @@ -5895,13 +5895,13 @@ dependencies = [ [[package]] name = "windows-service" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a" +checksum = "857224b3b211c6f3616921f081ee54721ee3ad2ace2fac6a6337e032f7b4dcf2" dependencies = [ "bitflags 2.13.1", "widestring", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/crates/pf-client-core/Cargo.toml b/crates/pf-client-core/Cargo.toml index 2c18d8ae..8546e873 100644 --- a/crates/pf-client-core/Cargo.toml +++ b/crates/pf-client-core/Cargo.toml @@ -150,7 +150,7 @@ libloading = "0.8" x11rb = { version = "0.14", default-features = false } [target.'cfg(windows)'.dependencies] -wasapi = "0.23" +wasapi = "0.24" # Native D3D11VA decode (M5 of the native-decode program): the hand-declared DXVA buffer # layouts and the AuPlan → picparams/qmatrix/slice-control conversion that video_d3d11_native # submits. Windows-only because the rung is; the crate itself is cross-platform CPU code so diff --git a/crates/pf-client-core/src/audio_wasapi.rs b/crates/pf-client-core/src/audio_wasapi.rs index 765281c1..5d646ccc 100644 --- a/crates/pf-client-core/src/audio_wasapi.rs +++ b/crates/pf-client-core/src/audio_wasapi.rs @@ -100,12 +100,14 @@ pub fn devices() -> Result<(Vec, Vec)> { /// audio keeps working, like the PipeWire twin's `target.object` behavior. /// Resolve an active endpoint by id WITHOUT `DeviceEnumerator::get_device`. /// -/// That helper builds its argument as `PCWSTR::from_raw(HSTRING::from(id).as_ptr())` — the -/// `HSTRING` is a temporary, dropped at the end of that statement, so `GetDevice` reads freed -/// memory and misses ids that are perfectly valid. Scanning the active collection touches only -/// safe crate APIs, so it cannot regress the same way. (`punktfunk-host` fixes the same bug with -/// raw COM instead; this crate cannot, because it pins a different `windows` revision than -/// `wasapi` does, making the two `IMMDevice` types incompatible.) +/// Through `wasapi 0.23` that helper built its argument as +/// `PCWSTR::from_raw(HSTRING::from(id).as_ptr())` — the `HSTRING` was a temporary, dropped at the +/// end of that statement, so `GetDevice` read freed memory and missed ids that are perfectly valid. +/// `wasapi 0.24` fixed that upstream. Scanning the active collection touches only safe crate APIs, +/// so it cannot regress the same way, and it additionally filters to ACTIVE endpoints — which is +/// why it stays. (`punktfunk-host` routes around the same bug with raw COM instead; this crate +/// cannot, because it pins a different `windows` revision than `wasapi` does, making the two +/// `IMMDevice` types incompatible.) pub(crate) fn device_by_id( enumerator: &DeviceEnumerator, direction: &Direction, diff --git a/crates/pf-client-core/src/pad_audio.rs b/crates/pf-client-core/src/pad_audio.rs index 16394481..d9de834c 100644 --- a/crates/pf-client-core/src/pad_audio.rs +++ b/crates/pf-client-core/src/pad_audio.rs @@ -920,9 +920,10 @@ fn pad_render_thread( let res = (|| -> anyhow::Result<()> { const BLOCK_ALIGN: usize = PAD_CHANNELS * 4; // f32 interleaved let enumerator = wasapi::DeviceEnumerator::new().context("DeviceEnumerator")?; - // Not `get_device`: that helper resolves through a freed string — see - // [`crate::audio::device_by_id`] (audio_wasapi.rs, mounted as `crate::audio` on - // Windows by lib.rs's `#[path]` swap — there is no `audio_wasapi` module name). + // Not `get_device`: that helper resolved through a freed string through wasapi 0.23, and + // this path additionally wants the ACTIVE-only filter — see [`crate::audio::device_by_id`] + // (audio_wasapi.rs, mounted as `crate::audio` on Windows by lib.rs's `#[path]` swap — + // there is no `audio_wasapi` module name). let device = crate::audio::device_by_id(&enumerator, &Direction::Render, endpoint_id) .map_err(|e| anyhow!("correlated endpoint not found: {e:#}"))?; let mut audio_client = device.get_iaudioclient().context("IAudioClient")?; diff --git a/crates/punktfunk-host/Cargo.toml b/crates/punktfunk-host/Cargo.toml index 7294ce7f..16d1e687 100644 --- a/crates/punktfunk-host/Cargo.toml +++ b/crates/punktfunk-host/Cargo.toml @@ -278,7 +278,10 @@ windows = { version = "0.62", features = [ # The SCM plumbing for the `service` subcommand (define_windows_service! / dispatcher / control # handler / ServiceManager install). Wraps the Win32 service API; the supervision loop itself uses # the `windows` crate above. -windows-service = "0.7" +# ⭐ Keep this at 0.8+: 0.7 was the LAST crate in the tree pulling `windows-sys 0.52`, so it alone +# kept a fourth windows-sys major compiling. 0.8.1 moves to `windows-sys 0.61`, which the tree +# already builds, and that duplicate is gone. (0.8.0 is NOT enough — it lands on 0.59.) +windows-service = "0.8" # Read the GOG.com install registry (HKLM\SOFTWARE\WOW6432Node\GOG.com\Games) for the GOG store # provider — ergonomic + correct-by-construction vs. hand-rolled Reg* FFI for subkey enumeration. winreg = "0.56" @@ -286,7 +289,7 @@ winreg = "0.56" # provider — a small read-only DOM is all we need (Identity/Executable/ShellVisuals/StoreId). roxmltree = "0.21" # WASAPI loopback audio capture (default render endpoint -> 48 kHz stereo f32 for the Opus path). -wasapi = "0.23" +wasapi = "0.24" # Shared host<->driver wire contract for the pf-vdisplay IddCx virtual-display backend: the # control-plane IOCTL codes + `#[repr(C)] Pod` request/reply structs, defined ONCE so host<->driver # ABI drift is a compile error (used from `capture.rs`). The `bytemuck` that serializes those diff --git a/crates/punktfunk-host/src/audio/windows/audio_control.rs b/crates/punktfunk-host/src/audio/windows/audio_control.rs index 1f4f2be2..c05fc116 100644 --- a/crates/punktfunk-host/src/audio/windows/audio_control.rs +++ b/crates/punktfunk-host/src/audio/windows/audio_control.rs @@ -509,9 +509,10 @@ pub(crate) fn restore_default_playback() { /// Open a device by endpoint id, with a name for error context. /// -/// Resolves through [`super::pad_endpoint::open_wasapi_device`], NOT the `wasapi` crate's -/// `DeviceEnumerator::get_device` — that one hands `GetDevice` a freed string (see the helper's -/// docs), so it fails at random on ids that are perfectly valid. +/// Resolves through [`super::pad_endpoint::open_wasapi_device`] rather than the `wasapi` crate's +/// `DeviceEnumerator::get_device`: that one handed `GetDevice` a freed string through 0.23, so it +/// failed at random on ids that are perfectly valid. `wasapi 0.24` fixed that, but we keep the one +/// resolution path — see the helper's docs. pub(crate) fn open_endpoint(ep: &Endpoint) -> Result { super::pad_endpoint::open_wasapi_device(&ep.1) .map_err(|e| anyhow!("open endpoint {:?}: {e:#}", ep.0)) diff --git a/crates/punktfunk-host/src/audio/windows/pad_endpoint.rs b/crates/punktfunk-host/src/audio/windows/pad_endpoint.rs index 4bc65054..383ce98e 100644 --- a/crates/punktfunk-host/src/audio/windows/pad_endpoint.rs +++ b/crates/punktfunk-host/src/audio/windows/pad_endpoint.rs @@ -911,13 +911,17 @@ fn open_mmdevice(endpoint_id: &str) -> Result { /// Open a [`wasapi::Device`] for an endpoint id WITHOUT the crate's `DeviceEnumerator::get_device`. /// -/// `wasapi 0.23` builds that call's argument as -/// `PCWSTR::from_raw(HSTRING::from(device_id).as_ptr())`. The `HSTRING` is a temporary, so it is -/// dropped at the end of THAT statement and `IMMDeviceEnumerator::GetDevice` reads freed memory on -/// the next line. Whether the endpoint is found then depends on what the allocator happened to +/// Through `wasapi 0.23` that call built its argument as +/// `PCWSTR::from_raw(HSTRING::from(device_id).as_ptr())`. The `HSTRING` was a temporary, so it was +/// dropped at the end of THAT statement and `IMMDeviceEnumerator::GetDevice` read freed memory on +/// the next line. Whether the endpoint was found then depended on what the allocator happened to /// leave behind — a heisenbug whose failure mode is `0x80070002` (ERROR_FILE_NOT_FOUND) for an id -/// that is perfectly valid. [`open_mmdevice`] keeps its wide buffer alive across the call, so -/// resolve there and only borrow the crate's wrapper around the resulting interface. +/// that is perfectly valid. **`wasapi 0.24` fixed this upstream** (the `HSTRING` is now bound to a +/// local that outlives the call), so this helper is no longer load-bearing for correctness. +/// +/// We still resolve here, because the `IMMDevice` is wanted in its own right: [`probe_activation`] +/// and the property-store readers below need the raw interface, so routing every lookup through +/// [`open_mmdevice`] keeps ONE resolution path whose errors name the endpoint id. pub(crate) fn open_wasapi_device(endpoint_id: &str) -> Result { let dev = open_mmdevice(endpoint_id)?; wasapi::Device::from_immdevice(dev) diff --git a/crates/punktfunk-host/src/audio/windows/wasapi_cap.rs b/crates/punktfunk-host/src/audio/windows/wasapi_cap.rs index 3e29b0c9..58b42db9 100644 --- a/crates/punktfunk-host/src/audio/windows/wasapi_cap.rs +++ b/crates/punktfunk-host/src/audio/windows/wasapi_cap.rs @@ -747,9 +747,10 @@ enum DefaultKind { Unknown, } -/// Resolves through [`super::pad_endpoint::open_wasapi_device`], NOT the `wasapi` crate's -/// `DeviceEnumerator::get_device` — that one hands `GetDevice` a freed string (see the helper's -/// docs), and a spurious miss here silently downgrades a capturable default to `Unknown`. +/// Resolves through [`super::pad_endpoint::open_wasapi_device`] rather than the `wasapi` crate's +/// `DeviceEnumerator::get_device`: that one handed `GetDevice` a freed string through 0.23, and a +/// spurious miss here silently downgrades a capturable default to `Unknown`. `wasapi 0.24` fixed +/// that, but we keep the one resolution path — see the helper's docs. fn judge_default(wiring: &wiring_plan::Wiring, id: &str) -> DefaultKind { let Ok(dev) = super::pad_endpoint::open_wasapi_device(id) else { return DefaultKind::Unknown; diff --git a/crates/punktfunk-tray/Cargo.toml b/crates/punktfunk-tray/Cargo.toml index 14e33a4b..3244ef53 100644 --- a/crates/punktfunk-tray/Cargo.toml +++ b/crates/punktfunk-tray/Cargo.toml @@ -38,8 +38,9 @@ rustls = { version = "0.23", default-features = false, features = ["aws_lc_rs", punktfunk-core = { path = "../punktfunk-core", default-features = false, features = ["tls", "ureq-tls"] } [target.'cfg(windows)'.dependencies] -# SCM QUERY_STATUS works unprivileged — the service-state probe. Same crate the host service uses. -windows-service = "0.7" +# SCM QUERY_STATUS works unprivileged — the service-state probe. Same crate the host service uses, +# and it must stay in lockstep with it — see the windows-sys note on the host's declaration. +windows-service = "0.8" windows = { version = "0.62", features = [ "Win32_Foundation", "Win32_Graphics_Gdi",