diff --git a/Cargo.lock b/Cargo.lock
index 03f97493..c3a427ac 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -121,18 +121,6 @@ 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"
@@ -308,6 +296,12 @@ dependencies = [
"libloading",
]
+[[package]]
+name = "assert_no_alloc"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55ca83137a482d61d916ceb1eba52a684f98004f18e0cafea230fe5579c178a3"
+
[[package]]
name = "assert_type_match"
version = "0.1.1"
@@ -501,6 +495,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"arboard",
+ "assert_no_alloc",
"avian3d",
"benilla-assets",
"benilla-formats",
@@ -511,8 +506,8 @@ dependencies = [
"bevy",
"bevy_egui",
"bytemuck",
+ "coreaudio-rs 0.13.0",
"cosmic-text",
- "cpal 0.17.1",
"crossbeam-channel",
"fontdb",
"half",
@@ -523,6 +518,8 @@ dependencies = [
"mint",
"objc2 0.5.2",
"objc2-app-kit 0.2.2",
+ "objc2-core-audio",
+ "objc2-core-audio-types",
"objc2-foundation 0.2.2",
"rand 0.8.6",
"raw-window-handle",
@@ -865,7 +862,7 @@ dependencies = [
"bevy_reflect",
"bevy_transform",
"coreaudio-sys",
- "cpal 0.15.3",
+ "cpal",
"rodio",
"tracing",
]
@@ -2644,14 +2641,14 @@ version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
dependencies = [
- "alsa 0.9.1",
+ "alsa",
"core-foundation-sys",
"coreaudio-rs 0.11.3",
"dasp_sample",
"jni 0.21.1",
"js-sys",
"libc",
- "mach2 0.4.3",
+ "mach2",
"ndk 0.8.0",
"ndk-context",
"oboe",
@@ -2661,36 +2658,6 @@ 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"
@@ -4111,7 +4078,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "065632c16b39f4c00bc5514de0698474d1b6c36120743a154c78ca27ffc1ffe1"
dependencies = [
"atomic-arena",
- "cpal 0.17.1",
"glam 0.33.2",
"mint",
"pastey",
@@ -4281,15 +4247,6 @@ 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"
@@ -4715,16 +4672,6 @@ 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"
@@ -4759,7 +4706,6 @@ dependencies = [
"objc2 0.6.4",
"objc2-core-audio-types",
"objc2-core-foundation",
- "objc2-foundation 0.3.2",
]
[[package]]
@@ -4791,9 +4737,7 @@ 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",
]
@@ -4860,8 +4804,6 @@ 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",
]
@@ -5697,7 +5639,7 @@ version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1"
dependencies = [
- "cpal 0.15.3",
+ "cpal",
"lewton",
]
diff --git a/crates/benilla-app/Cargo.toml b/crates/benilla-app/Cargo.toml
index 79482032..ab73e9c5 100644
--- a/crates/benilla-app/Cargo.toml
+++ b/crates/benilla-app/Cargo.toml
@@ -22,7 +22,7 @@ default = ["dev"]
# re-exported here (1175). Their `dev` is default-on too, so without the round trip a
# `--no-default-features` build gets the feature back through unification — and the binary
# carries the build machine's source-tree path after all. Same trap as the shim's, one level down.
-dev = ["benilla-assets/dev", "benilla-formats/dev", "benilla-world/dev", "dep:bevy_egui", "dep:libc"]
+dev = ["benilla-assets/dev", "benilla-formats/dev", "benilla-world/dev", "dep:bevy_egui"]
# Opt-in Tracy profiling: `cargo run --features tracy` streams per-system `info_span!` markers (on the
# hot systems) + render-pass CPU timings to a Tracy capture (decision 0026 — the dev/player seam;
# a `docs/howto/running.md` this once cited never landed).
@@ -61,9 +61,10 @@ include_dir = "0.7"
# `getrusage` — whole-process CPU time (all threads), the perf probes' `cpu_ms`/`cpu_pct` metric
# (`perf::process_cpu_secs`). Wall-clock fps is unusable as a regression instrument on a machine
# running parallel sessions' builds; CPU-per-frame is, and it is also the number the Mac report
-# is about ("250 % CPU at 59 fps"). Already in the tree transitively; declared so the call site is
-# an honest dependency.
-libc = { version = "0.2", optional = true }
+# is about ("250 % CPU at 59 fps"). Was dev-only; since 1857 the output backend needs it in every
+# build too (`mach_absolute_time`, the host clock its meters read; `thread_policy_set`, the render
+# thread's realtime policy).
+libc = "0.2"
# Resample varying-size layer BLPs to a uniform size for the terrain texture array; `png` so we can
# encode the cursor image into data an NSImage can load (macOS hardware cursor).
image = { version = "0.25", default-features = false, features = ["png"] }
@@ -97,11 +98,16 @@ cosmic-text = "0.16"
# public-in-crate types). Pinned to the exact version `cosmic-text` 0.16 depends on so there's one
# `fontdb` in the graph.
fontdb = "0.23"
-# kira's own backend dependency, named directly so the mixer can hand cpal an explicit device buffer
-# size instead of accepting `BufferSize::Default` (decision 1026). `CpalBackendSettings`' fields ARE
-# cpal types, so this must stay the exact version kira resolves — one cpal in the graph.
-cpal = "0.17"
-kira = { version = "0.12", default-features = false, features = ["cpal", "mp3", "wav", "pcm", "adpcm"] }
+# 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 = { 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
+# realtime-safety regression fails a test or a smoke instead of reaching an ear. Release builds
+# compile it out (`disable_release`, the crate's default) and carry no allocator wrapper.
+assert_no_alloc = "1.1"
# kira's own decoder, named directly for one trait: the mixer feeds streams through its own
# `MediaSource` so kira's per-stream decode thread runs OUR code and can promote its QoS (decision
# 1109 — a bare-spawned decode thread starves under the world-entry burst and zero-fills the mix).
@@ -141,6 +147,13 @@ smithay-clipboard = "0.7"
# hide revert to the arrow); we bypass it with NSCursor + NSWindow::disableCursorRects. See
# docs/knowledge/camera-control.md.
[target.'cfg(target_os = "macos")'.dependencies]
+# The audio device layer (decision 1857, `sound::output::coreaudio`): the HAL output unit through
+# coreaudio-rs's `AudioUnit`, and the AudioObject property/listener API through the objc2 CoreAudio
+# bindings — the two crates cpal itself stood on, now used directly. The versions are the ones the
+# graph already carries; the feature sets are the ones cpal proved to compile against.
+coreaudio-rs = { version = "0.13", default-features = false, features = ["audio_unit", "core_audio"] }
+objc2-core-audio = { version = "0.3", default-features = false, features = ["std", "AudioHardware"] }
+objc2-core-audio-types = { version = "0.3", default-features = false, features = ["std", "CoreAudioBaseTypes"] }
objc2 = "0.5"
objc2-app-kit = { version = "0.2", features = [
"NSApplication",
diff --git a/crates/benilla-app/assets/ui/KeyBindingsPage.xml b/crates/benilla-app/assets/ui/KeyBindingsPage.xml
index 438f1c11..bb5fe601 100644
--- a/crates/benilla-app/assets/ui/KeyBindingsPage.xml
+++ b/crates/benilla-app/assets/ui/KeyBindingsPage.xml
@@ -320,6 +320,10 @@
end
FauxScrollFrame_Update(scroll, table.getn(page.items),
KEYBINDINGS_DISPLAYED, KEYBINDINGS_ROW_HEIGHT)
+ -- The trough is OURS — the reference has no trough at all, so nothing in its kit shows
+ -- one. The owner pairs it with the bar the reference's `_Update` just decided about
+ -- (1860); `BenillaScrollBar_SetShown` used to do it and is off this path now.
+ BenillaScrollTrough_Follow(scroll:GetName())
KeyBindings_UpdateUnbindKey()
end
diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml
index 0fb88a6d..34096572 100644
--- a/crates/benilla-app/assets/ui/OptionsFrame.xml
+++ b/crates/benilla-app/assets/ui/OptionsFrame.xml
@@ -3471,10 +3471,34 @@
-
- ScrollFrame_OnLoad()
- this.updateFunc = KeyBindingsPage_Update
-
+ ScrollFrame_OnLoad()
+
+ FauxScrollFrame_OnVerticalScroll(KEYBINDINGS_ROW_HEIGHT, KeyBindingsPage_Update)
+
+
+ if not KeyBindingsPage.selected then
+ ScrollFrameTemplate_OnMouseWheel(arg1)
+ end
+
diff --git a/crates/benilla-app/assets/ui/ReputationFrame.xml b/crates/benilla-app/assets/ui/ReputationFrame.xml
index 406e6c3b..2d5196b7 100644
--- a/crates/benilla-app/assets/ui/ReputationFrame.xml
+++ b/crates/benilla-app/assets/ui/ReputationFrame.xml
@@ -329,7 +329,11 @@
-- `offset * valueStep`, so this SetValue writes the value the Slider already holds and
-- its change-gate (decision 0250) drops it before it could re-enter this function.
-- Kept because it is the reference's line, not because it does anything.
- if not FauxScrollFrame_Update(scrollFrame, numFactions, NUM_FACTIONS_DISPLAYED, REPUTATIONFRAME_FACTIONHEIGHT) then
+ local showBar = FauxScrollFrame_Update(scrollFrame, numFactions, NUM_FACTIONS_DISPLAYED, REPUTATIONFRAME_FACTIONHEIGHT)
+ -- Our trough is not the reference's business: it follows the bar the ref just decided
+ -- about (1860).
+ BenillaScrollTrough_Follow("ReputationListScrollFrame")
+ if not showBar then
getglobal("ReputationListScrollFrameScrollBar"):SetValue(0)
end
local factionOffset = FauxScrollFrame_GetOffset(scrollFrame)
@@ -435,9 +439,8 @@
function ReputationFrame_OnLoad()
local frame = this
frame:RegisterEvent("UPDATE_FACTION")
- -- The faux kit's one wiring requirement: the owner names its own repaint so a drag of
- -- the scroll bar re-binds the rows (ScrollTemplates.xml's `frame.updateFunc` contract).
- getglobal("ReputationListScrollFrame").updateFunc = ReputationFrame_Update
+ -- The repaint is wired in the scroll frame's own `` below, which is
+ -- where the reference's kit puts it (1860).
end
function ReputationFrame_OnShow()
@@ -744,11 +747,19 @@
+ page. Its `` is what re-binds the rows (1860). -->
-
+
+
+ FauxScrollFrame_OnVerticalScroll(REPUTATIONFRAME_FACTIONHEIGHT, ReputationFrame_Update)
+
+
-
-
-
-
-
-
-
-
-
-
- ScrollFrame_OnLoad()
-
-
+
diff --git a/crates/benilla-app/assets/ui/TrainerFrame.xml b/crates/benilla-app/assets/ui/TrainerFrame.xml
index 75225504..b569a6f5 100644
--- a/crates/benilla-app/assets/ui/TrainerFrame.xml
+++ b/crates/benilla-app/assets/ui/TrainerFrame.xml
@@ -492,8 +492,8 @@
frame:RegisterEvent("TRAINER_UPDATE")
frame:RegisterEvent("TRAINER_CLOSED")
frame:RegisterEvent("PLAYER_MONEY")
- -- The faux-scroll frame repaints the list through this window's Update.
- getglobal("BenillaTrainerListScrollFrame").updateFunc = BenillaTrainerFrame_Update
+ -- The list repaint is wired in the scroll frame's own `` below, the
+ -- reference's way; `updateFunc` was our kit's field and the reference has none (1860).
end
-- Push the three saved globals into the engine — the ref's ClassTrainerFrame
@@ -508,6 +508,14 @@
SetTrainerServiceTypeFilter("used", TRAINER_FILTER_USED)
end
+ -- The scroll frame's `` lives in its own chunk and cannot see this
+ -- file's `local BENILLA_TRAINER_ROW_HEIGHT`, so the pitch is handed over here rather than
+ -- written as a literal in the XML (1860). The reference has the same shape with globals:
+ -- `QUESTLOG_QUEST_HEIGHT` and friends are file-scope globals in its own FrameXML.
+ function BenillaTrainerList_OnVerticalScroll()
+ FauxScrollFrame_OnVerticalScroll(BENILLA_TRAINER_ROW_HEIGHT, BenillaTrainerFrame_Update)
+ end
+
function BenillaTrainerFrame_SetTitle(name)
if name and name ~= "" then
getglobal("BenillaTrainerTitleText"):SetText(name)
@@ -518,6 +526,14 @@
function BenillaTrainerFrame_OnEvent(frame)
if event == "TRAINER_SHOW" then
+ -- SHOW FIRST, then populate — the reference's own order, and not a style choice.
+ -- `FauxScrollFrame_Update` (the chain's since 1837) gates everything that sets the
+ -- bar's range on `frame:IsVisible()`, so a list repainted before its window is up
+ -- leaves `SetMinMaxValues` unset and both arrows dead. The reference relies on this
+ -- order hard enough to test it: `MerchantFrame.lua:22-27` does `ShowUIPanel(this)`
+ -- and then treats `not this:IsVisible()` as the panel system refusing, and closes.
+ -- Decision 1860.
+ ShowUIPanel(frame)
-- Fresh session: top of the list, a first selection (ref ClassTrainerFrame_Show).
BenillaTrainerFrame_ApplyFilter()
BenillaTrainerFrame_SetTitle(arg1)
@@ -526,7 +542,6 @@
selectFirstService()
end
BenillaTrainerFrame_Update()
- ShowUIPanel(frame)
elseif event == "TRAINER_UPDATE" then
BenillaTrainerFrame_SetTitle(arg1)
-- Re-anchor the selection if it fell off the (possibly shorter) list.
@@ -793,13 +808,25 @@
-
+
+
+ BenillaTrainerList_OnVerticalScroll()
+
-
+