From f83533b2a65065a0a1750cd3443dde7d337d46df Mon Sep 17 00:00:00 2001 From: Sam Sung <51686767+samwhosung@users.noreply.github.com> Date: Sat, 17 Oct 2026 15:30:35 +0700 Subject: [PATCH] Linux audio plays: cpal 0.17.3 fixes ALSA's stalled callback (#1785) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On some Linux setups the game was silent. The device opened, but its data callback ran once and stopped, with no error. The music position never moved. This is cpal 0.17.1's ALSA bug (RustAudio/cpal#1099, "Data callback called only once on ALSA"), fixed in 0.17.3 by setting playback's `avail_min` to one period. benilla's stream starts only at the start threshold, which is exactly that case. cpal goes to 0.17.3. alsa 0.11 and bevy_audio's unbuilt cpal 0.15 cannot share a lock (`links = "alsa"`). A lock-only stand-in for alsa 0.9 settles that, documented in `third_party/alsa-0.9`. The realtime-fallback log line now says what happens on each platform. Following the default device gets its `Deviation:` line: 1.12.1 initialises sound once (`0x7a4330`). Built on macOS, with Linux and Windows linted. Fixes #820 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (cherry picked from commit d93c231d8ebb27934791b726a0575f4c9b709446) # Conflicts: # Cargo.toml # crates/benilla-app/examples/extended_launcher.rs # docs/DEVIATIONS.md Absorbed into the octo fork. Cargo.toml unions both patch entries (our mlua-sys traceback patch and upstream's alsa 0.9 lock-only stand-in). Cargo.lock is regenerated locally rather than taken from upstream, with cargo update -p cpal@0.17.1 --precise 0.17.3: cargo reaches the same resolution (alsa 0.9.1 from the stand-in, alsa 0.11 + alsa-sys 0.4 for the built cpal, windows-* dropped), so it is our own lock, resolved here. examples/extended_launcher.rs is upstream-only and stays absent. cargo check -p benilla-app clean. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- Cargo.lock | 148 +++++--------------- Cargo.toml | 3 + crates/benilla-app/src/sound/output/cpal.rs | 7 + crates/benilla-app/src/sound/output/mod.rs | 12 +- third_party/alsa-0.9/BENILLA.md | 18 +++ third_party/alsa-0.9/Cargo.toml | 6 + third_party/alsa-0.9/src/lib.rs | 4 + 7 files changed, 83 insertions(+), 115 deletions(-) create mode 100644 third_party/alsa-0.9/BENILLA.md create mode 100644 third_party/alsa-0.9/Cargo.toml create mode 100644 third_party/alsa-0.9/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index a10a4220..58a145c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,20 +112,12 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alsa" version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" -dependencies = [ - "alsa-sys", - "bitflags 2.11.1", - "cfg-if", - "libc", -] [[package]] name = "alsa" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c88dbbce13b232b26250e1e2e6ac18b6a891a646b8148285036ebce260ac5c3" +checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" dependencies = [ "alsa-sys", "bitflags 2.11.1", @@ -135,9 +127,9 @@ dependencies = [ [[package]] name = "alsa-sys" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04" dependencies = [ "libc", "pkg-config", @@ -521,7 +513,7 @@ dependencies = [ "bytemuck", "coreaudio-rs 0.13.0", "cosmic-text", - "cpal 0.17.1", + "cpal 0.17.3", "crossbeam-channel", "fontdb", "half", @@ -2624,6 +2616,20 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "coreaudio-rs" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" +dependencies = [ + "bitflags 2.11.1", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", +] + [[package]] name = "coreaudio-sys" version = "0.2.17" @@ -2682,12 +2688,12 @@ dependencies = [ [[package]] name = "cpal" -version = "0.17.1" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1f9c7312f19fc2fa12fd7acaf38de54e8320ba10d1a02dcbe21038def51ccb" +checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7" dependencies = [ - "alsa 0.10.0", - "coreaudio-rs 0.13.0", + "alsa 0.11.0", + "coreaudio-rs 0.14.2", "dasp_sample", "jni 0.21.1", "js-sys", @@ -2707,7 +2713,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows 0.62.2", + "windows 0.61.3", ] [[package]] @@ -3110,7 +3116,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3368,8 +3374,8 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link 0.2.1", - "windows-result 0.4.1", + "windows-link 0.1.3", + "windows-result 0.3.4", ] [[package]] @@ -3461,7 +3467,7 @@ dependencies = [ "vec_map", "wasm-bindgen", "web-sys", - "windows 0.62.2", + "windows 0.54.0", ] [[package]] @@ -3968,7 +3974,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4567,7 +4573,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5806,7 +5812,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7441,7 +7447,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7476,23 +7482,11 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections 0.2.0", + "windows-collections", "windows-core 0.61.2", - "windows-future 0.2.1", + "windows-future", "windows-link 0.1.3", - "windows-numerics 0.2.0", -] - -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections 0.3.2", - "windows-core 0.62.2", - "windows-future 0.3.2", - "windows-numerics 0.3.1", + "windows-numerics", ] [[package]] @@ -7504,15 +7498,6 @@ dependencies = [ "windows-core 0.61.2", ] -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core 0.62.2", -] - [[package]] name = "windows-core" version = "0.54.0" @@ -7549,19 +7534,6 @@ dependencies = [ "windows-strings 0.4.2", ] -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - [[package]] name = "windows-future" version = "0.2.1" @@ -7570,18 +7542,7 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading 0.1.0", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core 0.62.2", - "windows-link 0.2.1", - "windows-threading 0.2.1", + "windows-threading", ] [[package]] @@ -7650,16 +7611,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core 0.62.2", - "windows-link 0.2.1", -] - [[package]] name = "windows-result" version = "0.1.2" @@ -7687,15 +7638,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-strings" version = "0.1.0" @@ -7715,15 +7657,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-sys" version = "0.45.0" @@ -7800,15 +7733,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" diff --git a/Cargo.toml b/Cargo.toml index ed50fae9..038a0b84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,9 @@ kira = { path = "third_party/kira" } # cost ~7 ms a raise — invisibly, for the raises an addon wraps in `pcall`: five a frame measured # 44.7 ms of VM tick with an empty error log (`third_party/mlua-sys/BENILLA.md`). mlua-sys = { path = "third_party/mlua-sys" } +# A lock-only stand-in: bevy_audio's unbuilt cpal 0.15 would otherwise hold `alsa-sys` 0.3 against +# cpal 0.17's 0.4, and a lock holds one `links = "alsa"` (`third_party/alsa-0.9/BENILLA.md`). +alsa = { path = "third_party/alsa-0.9" } # Dependencies at opt-level 3 in dev: at opt-level 0, avian/parry collision drops a run to <1 fps. # Line tables only, and never packed into the binary, for the reason under `[profile.dev]` below. diff --git a/crates/benilla-app/src/sound/output/cpal.rs b/crates/benilla-app/src/sound/output/cpal.rs index 356edfe0..c5490a19 100644 --- a/crates/benilla-app/src/sound/output/cpal.rs +++ b/crates/benilla-app/src/sound/output/cpal.rs @@ -332,6 +332,13 @@ pub(super) struct Notices { /// The device watch: a thread that re-reads the host's default output every [`WATCH_EVERY`] and /// raises a notice when it changes. One-shot: the backend answers a notice by rebuilding the /// stream, which arms a fresh watch. +/// +/// Deviation: a change of the default output device is followed (here and in `coreaudio.rs`), where +/// 1.12.1 initialises its sound device once and never again (`Sound::Initialize` `0x7a4330` +/// refuses a second call; the driver-enumeration functions `0x7a6320`-`0x7a6380` have no caller), +/// because a headset plugged in mid-session should play at once. On ALSA the watch sees only the +/// "default" PCM, whose id never changes, so on Linux it is the sound server (PipeWire or Pulse) +/// that moves the stream. pub(super) struct Listeners { stop: Arc, thread: Option>, diff --git a/crates/benilla-app/src/sound/output/mod.rs b/crates/benilla-app/src/sound/output/mod.rs index 29513f33..ab9e5178 100644 --- a/crates/benilla-app/src/sound/output/mod.rs +++ b/crates/benilla-app/src/sound/output/mod.rs @@ -650,8 +650,8 @@ fn render_loop( period_ns: u64, ) { // Held for the thread's life: on Windows its drop hands the MMCSS registration back. - // `WOW_AUDIO_NO_RT=1` skips realtime for the user-interactive QoS fallback, an A/B lever for - // the render thread preempting the compute pools. + // `WOW_AUDIO_NO_RT=1` skips realtime for the fallback (user-interactive QoS on macOS), an A/B + // lever for the render thread preempting the compute pools. let no_rt = std::env::var_os("WOW_AUDIO_NO_RT").is_some(); let realtime = if no_rt { Err(anyhow::anyhow!("WOW_AUDIO_NO_RT=1")) @@ -667,7 +667,13 @@ fn render_loop( Some(realtime) } Err(e) => { - warn!("audio: render thread could not go realtime ({e}); using user-interactive QoS"); + // `promote_current_thread` is a no-op off macOS, so the thread keeps its normal priority. + let fallback = if cfg!(target_os = "macos") { + "user-interactive QoS" + } else { + "normal thread priority" + }; + warn!("audio: render thread could not go realtime ({e}); using {fallback}"); benilla_world::thread_qos::promote_current_thread( benilla_world::thread_qos::QosClass::UserInteractive, ); diff --git a/third_party/alsa-0.9/BENILLA.md b/third_party/alsa-0.9/BENILLA.md new file mode 100644 index 00000000..79571728 --- /dev/null +++ b/third_party/alsa-0.9/BENILLA.md @@ -0,0 +1,18 @@ +# A lock-only stand-in for `alsa` 0.9.1 + +Written by benilla, MIT OR Apache-2.0; it contains no upstream code. Wired in through +`[patch.crates-io]` in the workspace root. + +Why: `alsa-sys` declares `links = "alsa"`, and a lockfile may hold one version of it. `cpal` 0.17.3, +our audio device layer off macOS, takes `alsa` 0.11 and so `alsa-sys` 0.4. `bevy_internal` lists +`bevy_audio` as a weak optional dependency (`bevy_audio?/android_shared_stdcxx`), which puts +`bevy_audio`, its `cpal` 0.15.3 and so `alsa` 0.9.1 and `alsa-sys` 0.3 into the lock although the +workspace never enables bevy's audio (`Cargo.toml`, `bevy`). The two `links` collide and the +resolver refuses. This package satisfies `cpal` 0.15.3's `alsa = "0.9"` with no dependency on +`alsa-sys`. + +It is never compiled: `cargo tree --target all` holds no `cpal` 0.15. If bevy's audio were enabled, +the build would stop at the `compile_error!` in `src/lib.rs`, which says why. + +Drop it when the lock holds no `bevy_audio` (a bevy that no longer lists it) or when bevy's +`cpal` takes `alsa` 0.11. diff --git a/third_party/alsa-0.9/Cargo.toml b/third_party/alsa-0.9/Cargo.toml new file mode 100644 index 00000000..e4d74eb3 --- /dev/null +++ b/third_party/alsa-0.9/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "alsa" +version = "0.9.1" +edition = "2021" +license = "MIT OR Apache-2.0" +description = "Lock-only stand-in for alsa 0.9.1; see BENILLA.md" diff --git a/third_party/alsa-0.9/src/lib.rs b/third_party/alsa-0.9/src/lib.rs new file mode 100644 index 00000000..a0f2da3b --- /dev/null +++ b/third_party/alsa-0.9/src/lib.rs @@ -0,0 +1,4 @@ +compile_error!( + "third_party/alsa-0.9 is a lock-only stand-in: bevy_audio and its cpal 0.15 are not built. \ + See third_party/alsa-0.9/BENILLA.md." +);