From b41acbf98a6369f709cbca73fa2acbb3d64106b0 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 4 Sep 2026 20:52:22 +0700 Subject: [PATCH] Linux and Windows get their sound back, and emotes refuse while moving MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - every build off macOS has been silent since the 09-02 snapshot (B356): the non-macOS device layer was a stub that opened nothing, and nothing had ever compiled the client for another target; that layer is now the same output stream over cpal — one seam, not two stacks: the ring, the render thread and the rebuild-on-device-change loop are one code on every platform, only the device file differs; the audio thread never allocates, a lost or changed default device is noticed at kira's own cadence, and realtime is `SCHED_FIFO` on unix and MMCSS "Pro Audio" on Windows, either refusable, the ring turning a refusal into a degradation; and the two instruments this bug says were missing exist — a Linux and Windows cross-compile under `clippy -D warnings`, and a live output test that opens the real device and asserts the ring never ran dry, because compiling is not sounding (1920) - an emote that requires standing still now refuses on the move: `CheckEmoteEligible`'s `0x4000` arm writes an out-param that `DoEmote` turns into a red "You can't do that while moving!" and aborts; 20 emotes of 78 can raise the line, read off the shipped `Emotes.dbc`; SWIMMING is not in the mask, so a swimmer holding still emotes fine, and `IsSelfControlled` is TRUE for an ordinary player, so a feared or confused player emotes away happily (1904); and the cast path gains its equipped-item rung — the button greyed and pressing it still sent: the reference short-circuits to "fits" on four gates, not one, the miss being a zero `EquippedItemSubClassMask`, which means no item required at all, and the rung sits above the crowd-control validator, so a player both stunned and missing the weapon is told about the weapon; that ladder has six arms, not three — CHARMED, FLEEING and CONFUSED join (1925) - the quest reward's money is the reference's own widget: the tail of the shared `QuestFrameItems_Update` drives `questState.."MoneyFrame"` through `MoneyFrame_Update`, and the two hand-built coin trios become one `MoneyFrameTemplate` instance each at the reference's anchors, which our coins already used; the progress panel stays on coins, being the required-money flow, not the reward one (1928); and the flight master's `!` joins the questgiver sweep, because the green `!` and the gold `!` are the SAME slot, `unit+0xb2c`, torn down once before both queries re-issue; the mouseover re-ask is refuted — the only taxi-status sender has one live caller — and the two queries are not symmetric: the questgiver's goes out on every sweep, the taxi one only when the unit's key moves or on a full sweep, as the wire shows at Dungar Longdrink: 68 questgiver status queries against 1 taxi-node one (1918) - `GetSpellTabInfo` out of range answers `nil, nil, 0, 0` — the reference returns four values on every path that returns (`0x4b3ce0`), and ours answered a single nil copied from another binding's shape; it matters because stock `SpellBookFrame_OnLoad` calls it unconditionally and then does `id > (offset + numSpells)`, which a nil raises on and (0+0) makes true, hiding every button on the page — the behaviour to match; the argument raises the reference's usage string when absent, and the index truncates toward zero THEN decrements (1931); the perf probe gains a `WOW_PROBE_UNCAP=vsync` arm, because the uncapped present mode is a request, not a schedule — measured, it taxes the number it was meant to pin (11.80 `cpu_ms` uncapped against 14.65 under it), so the default stays uncapped; and the last three test temp paths are keyed on the pid, after concurrent runs raced on one fixed shared file Co-Authored-By: Claude Fable 5.1 --- Cargo.lock | 75 ++- crates/benilla-app/Cargo.toml | 20 +- crates/benilla-app/assets/ui/QuestFrame.xml | 33 +- crates/benilla-app/src/capture/mod.rs | 12 + .../src/entities/attach/char_skin.rs | 2 +- crates/benilla-app/src/items.rs | 25 + crates/benilla-app/src/local_state.rs | 2 + crates/benilla-app/src/net.rs | 2 +- crates/benilla-app/src/net/motion/relay.rs | 2 +- crates/benilla-app/src/player.rs | 21 + crates/benilla-app/src/quest_markers/query.rs | 225 ++++++- .../benilla-app/src/sound/output/coreaudio.rs | 40 +- crates/benilla-app/src/sound/output/cpal.rs | 567 ++++++++++++++++++ crates/benilla-app/src/sound/output/mod.rs | 167 +++++- crates/benilla-app/src/sound/output/stub.rs | 89 --- crates/benilla-app/src/target/scan.rs | 2 +- crates/benilla-app/src/ui_action/cast_send.rs | 38 +- crates/benilla-app/src/ui_action/state.rs | 175 ++++-- crates/benilla-app/src/ui_action/usable.rs | 180 +++++- crates/benilla-app/src/ui_chat.rs | 2 +- crates/benilla-app/src/ui_chat/input/mod.rs | 77 ++- crates/benilla-app/src/ui_chat/tests.rs | 120 +++- crates/benilla-app/src/ui_items/drain.rs | 2 +- crates/benilla-app/src/ui_script/content.rs | 2 +- crates/benilla-app/src/ui_taxi/mod.rs | 141 +---- crates/benilla-app/src/ui_text/engine/gpu.rs | 2 +- crates/benilla-app/src/video.rs | 4 +- crates/benilla-formats/src/lib.rs | 3 +- crates/benilla-formats/tests/chain.rs | 8 +- crates/benilla-protocol/src/messages/items.rs | 2 +- .../benilla-protocol/src/messages/movement.rs | 4 +- .../src/messages/update_object/fields/mod.rs | 2 +- .../src/world/writer/items.rs | 2 +- crates/benilla-ui/src/script/dressup.rs | 2 +- crates/benilla-ui/src/script/measure.rs | 4 +- crates/benilla-ui/src/script/model.rs | 2 +- crates/benilla-ui/src/script/spellbook.rs | 109 +++- crates/benilla-ui/src/script/tooltip_unit.rs | 2 +- crates/benilla-ui/src/widget/propagation.rs | 2 +- crates/benilla-world/src/art_scope.rs | 2 +- 40 files changed, 1745 insertions(+), 426 deletions(-) create mode 100644 crates/benilla-app/src/sound/output/cpal.rs delete mode 100644 crates/benilla-app/src/sound/output/stub.rs diff --git a/Cargo.lock b/Cargo.lock index c3a427ac..4a0e4a52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,6 +121,18 @@ dependencies = [ "libc", ] +[[package]] +name = "alsa" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c88dbbce13b232b26250e1e2e6ac18b6a891a646b8148285036ebce260ac5c3" +dependencies = [ + "alsa-sys", + "bitflags 2.11.1", + "cfg-if", + "libc", +] + [[package]] name = "alsa-sys" version = "0.3.1" @@ -508,6 +520,7 @@ dependencies = [ "bytemuck", "coreaudio-rs 0.13.0", "cosmic-text", + "cpal 0.17.1", "crossbeam-channel", "fontdb", "half", @@ -862,7 +875,7 @@ dependencies = [ "bevy_reflect", "bevy_transform", "coreaudio-sys", - "cpal", + "cpal 0.15.3", "rodio", "tracing", ] @@ -2641,14 +2654,14 @@ version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" dependencies = [ - "alsa", + "alsa 0.9.1", "core-foundation-sys", "coreaudio-rs 0.11.3", "dasp_sample", "jni 0.21.1", "js-sys", "libc", - "mach2", + "mach2 0.4.3", "ndk 0.8.0", "ndk-context", "oboe", @@ -2658,6 +2671,36 @@ dependencies = [ "windows 0.54.0", ] +[[package]] +name = "cpal" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b1f9c7312f19fc2fa12fd7acaf38de54e8320ba10d1a02dcbe21038def51ccb" +dependencies = [ + "alsa 0.10.0", + "coreaudio-rs 0.13.0", + "dasp_sample", + "jni 0.21.1", + "js-sys", + "libc", + "mach2 0.5.0", + "ndk 0.9.0", + "ndk-context", + "num-derive", + "num-traits", + "objc2 0.6.4", + "objc2-audio-toolbox", + "objc2-avf-audio", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.62.2", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -4247,6 +4290,15 @@ dependencies = [ "libc", ] +[[package]] +name = "mach2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -4672,6 +4724,16 @@ dependencies = [ "objc2-foundation 0.3.2", ] +[[package]] +name = "objc2-avf-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -4706,6 +4768,7 @@ dependencies = [ "objc2 0.6.4", "objc2-core-audio-types", "objc2-core-foundation", + "objc2-foundation 0.3.2", ] [[package]] @@ -4737,7 +4800,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.11.1", + "block2 0.6.2", "dispatch2", + "libc", "objc2 0.6.4", ] @@ -4804,6 +4869,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.11.1", + "block2 0.6.2", + "libc", "objc2 0.6.4", "objc2-core-foundation", ] @@ -5639,7 +5706,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1" dependencies = [ - "cpal", + "cpal 0.15.3", "lewton", ] diff --git a/crates/benilla-app/Cargo.toml b/crates/benilla-app/Cargo.toml index ab73e9c5..dba96a5c 100644 --- a/crates/benilla-app/Cargo.toml +++ b/crates/benilla-app/Cargo.toml @@ -99,9 +99,11 @@ cosmic-text = "0.16" # `fontdb` in the graph. fontdb = "0.23" # kira renders the mix; the device it plays through is OURS (`sound::output`, decision 1857) — -# kira's own cpal backend is not enabled, and cpal is out of the graph on our path with it: on -# macOS it could neither report a lost default device nor rebuild on one, and it discarded the -# IO-cycle timestamp the deadline meters need. +# kira's own `cpal` backend is not enabled on any target. On macOS that took cpal out of the graph +# entirely: it could neither report a lost default device nor rebuild on one, and it discarded the +# IO-cycle timestamp the deadline meters need. Off macOS cpal is back (1920) but as OUR device +# layer under the same backend, not as kira's — the ring, the render thread and the meters are the +# same code on every platform. kira = { version = "0.12", default-features = false, features = ["mp3", "wav", "pcm", "adpcm"] } # The realtime-audio allocation tripwire (decision 1857): `assert_no_alloc(|| …)` around the output # IO callback and the render pass aborts a DEBUG build on any allocation inside them, so a @@ -167,3 +169,15 @@ objc2-app-kit = { version = "0.2", features = [ "NSEvent", ] } objc2-foundation = { version = "0.2", features = ["NSArray", "NSData", "NSGeometry"] } + +# Everywhere else: the same device layer over cpal (decision 1920, `sound::output::cpal`). 1857 +# owns the mix-ahead ring, the render thread and the meters on every platform; only the last +# hop — find the default device, open a stream on it, hand back one buffer per cycle — is +# platform code, and off macOS that hop is ALSA/PipeWire, WASAPI or the host's own API. cpal is +# the layer that already speaks all of them; on macOS it is still not in the graph, because +# there its defects (a no-op error callback on the default device, a pinned `DefaultOutput` +# unit, the discarded IO-cycle timestamp) are exactly what 1857 went direct to escape. On an +# unsupported target cpal compiles to its Null host, which reports no device — the same silent +# posture the old `stub.rs` had, without a second file to rot. +[target.'cfg(not(target_os = "macos"))'.dependencies] +cpal = "0.17" diff --git a/crates/benilla-app/assets/ui/QuestFrame.xml b/crates/benilla-app/assets/ui/QuestFrame.xml index ffaeb690..ca96cf9e 100644 --- a/crates/benilla-app/assets/ui/QuestFrame.xml +++ b/crates/benilla-app/assets/ui/QuestFrame.xml @@ -206,7 +206,7 @@ local t = { "QuestDescription", "QuestObjectiveText", "QuestDetailRewardTitleText", "QuestDetailItemChooseText", "QuestDetailItemReceiveText" } for i = 1, MAX_NUM_ITEMS do table.insert(t, "QuestDetailItem" .. i) end - for i = 1, 3 do table.insert(t, "QuestDetailMoneyCoin" .. i) end + table.insert(t, "QuestDetailMoneyFrame") return t end)(), ["QuestProgressScrollFrame"] = (function() @@ -220,7 +220,7 @@ local t = { "QuestRewardText", "QuestRewardRewardTitleText", "QuestRewardItemChooseText", "QuestRewardItemReceiveText" } for i = 1, MAX_NUM_ITEMS do table.insert(t, "QuestRewardItem" .. i) end - for i = 1, 3 do table.insert(t, "QuestRewardMoneyCoin" .. i) end + table.insert(t, "QuestRewardMoneyFrame") return t end)(), } @@ -358,6 +358,16 @@ HideUIPanel(getglobal("QuestFrame")) end + -- The reference's own `QuestMoneyFrame_OnLoad` (QuestFrame.lua:306-309), which lives in + -- THIS file in the reference too — so it is home, not a squatter. Its whole body is these + -- two calls. Needed because 1928 put the two reward panels on `MoneyFrameTemplate`, whose + -- instances call it from their OnLoad exactly as the reference's do. Both callees are our + -- `MoneyFrame.xml`'s, which the manifest loads at entry 141 — far above this file. + function QuestMoneyFrame_OnLoad() + MoneyFrame_OnLoad() + MoneyFrame_SetType("STATIC") + end + -- ── The shared reward grid — `QuestFrameItems_Update(questState)`, the REFERENCE'S OWN -- NAME and the reference's own algorithm (QuestFrame.lua:311-522), ported line-for-line. -- @@ -454,10 +464,17 @@ end end + -- The reference's own tail (QuestFrame.lua:350-356), on the reference's own widget. + -- `QuestFrame_SetAsLastShown` is deliberately NOT called: it anchors a SPACER FRAME + -- that this client does not have — our panels size their scroll child from + -- `BENILLA_QUEST_PANEL_WIDGETS` instead (see 1928). The money half converges here; the + -- sizing model stays ours until this window migrates. + local moneyFrame = getglobal(questState .. "MoneyFrame") if money and money > 0 then - BenillaMoney_Set(questState .. "Money", money, true) + moneyFrame:Show() + MoneyFrame_Update(questState .. "MoneyFrame", money) else - BenillaMoney_Clear(questState .. "Money") + moneyFrame:Hide() end end @@ -1154,9 +1171,7 @@ - - - + QuestMoneyFrame_OnLoad() @@ -1292,9 +1307,7 @@ - - - + QuestMoneyFrame_OnLoad()